From 8abe71a6a81c649e9adbd0c2ffc795c7c7d16a49 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 1 Jul 2026 14:41:31 +0200 Subject: [PATCH 01/15] Added ASN.1 OER encoding AI-Assisted: yes (Cursor AI) --- .config/ci/install.ps1 | 3 + .config/ci/install.sh | 3 + scapy/all.py | 1 + scapy/asn1/oer.py | 825 +++++++++++++++++++++++++++++++ scapy/asn1fields.py | 157 +++++- test/scapy/layers/asn1.uts | 154 ++++++ test/scapy/layers/oer_fuzz.py | 106 ++++ test/scapy/layers/oer_iop.py | 231 +++++++++ test/scapy/layers/oer_packets.py | 149 ++++++ 9 files changed, 1611 insertions(+), 18 deletions(-) create mode 100644 scapy/asn1/oer.py create mode 100644 test/scapy/layers/oer_fuzz.py create mode 100644 test/scapy/layers/oer_iop.py create mode 100644 test/scapy/layers/oer_packets.py diff --git a/.config/ci/install.ps1 b/.config/ci/install.ps1 index 5f9e5b58443..09fbab8969f 100644 --- a/.config/ci/install.ps1 +++ b/.config/ci/install.ps1 @@ -19,3 +19,6 @@ python -m pip install --upgrade pip setuptools wheel --ignore-installed # Make sure tox is installed and up to date python -m pip install -U tox --ignore-installed + +# Optional test dependency for ASN.1 OER interoperability tests +python -m pip install asn1tools diff --git a/.config/ci/install.sh b/.config/ci/install.sh index aa96f1a99b5..c22855a36a7 100755 --- a/.config/ci/install.sh +++ b/.config/ci/install.sh @@ -53,5 +53,8 @@ python -m pip install --upgrade pip setuptools wheel --ignore-installed # Make sure tox is installed and up to date python -m pip install -U tox --ignore-installed +# Optional test dependency for ASN.1 OER interoperability tests +python -m pip install asn1tools + # Dump Environment (so that we can check PATH, UT_FLAGS, etc.) set diff --git a/scapy/all.py b/scapy/all.py index e28f0a91c71..65752e8f5e5 100644 --- a/scapy/all.py +++ b/scapy/all.py @@ -41,6 +41,7 @@ from scapy.asn1.asn1 import * from scapy.asn1.ber import * +from scapy.asn1.oer import * from scapy.asn1.mib import * from scapy.pipetool import * diff --git a/scapy/asn1/oer.py b/scapy/asn1/oer.py new file mode 100644 index 00000000000..3bb1c1b75a2 --- /dev/null +++ b/scapy/asn1/oer.py @@ -0,0 +1,825 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +Octet Encoding Rules (OER) for ASN.1 + +Basic-OER as specified in ITU-T X.696 | ISO/IEC 8825-7. +""" + +import struct + +from scapy.error import warning +from scapy.compat import chb, orb, bytes_encode +from scapy.utils import binrepr, inet_aton, inet_ntoa +from scapy.asn1.ber import BER_num_dec, BER_num_enc +from scapy.asn1.asn1 import ( + ASN1Tag, + ASN1_BADTAG, + ASN1_BadTag_Decoding_Error, + ASN1_Class, + ASN1_Class_UNIVERSAL, + ASN1_Codecs, + ASN1_DECODING_ERROR, + ASN1_Decoding_Error, + ASN1_Encoding_Error, + ASN1_Error, + ASN1_Object, + _ASN1_ERROR, +) + +from typing import ( + Any, + AnyStr, + Dict, + Generic, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, +) + +################## +# OER encoding # +################## + + +class OER_Exception(Exception): + pass + + +class OER_Encoding_Error(ASN1_Encoding_Error): + def __init__(self, + msg, # type: str + encoded=None, # type: Optional[Union['OERcodec_Object[Any]', str]] + remaining=b"" # type: bytes + ): + # type: (...) -> None + Exception.__init__(self, msg) + self.remaining = remaining + self.encoded = encoded + + def __str__(self): + # type: () -> str + s = Exception.__str__(self) + if isinstance(self.encoded, ASN1_Object): + s += "\n### Already encoded ###\n%s" % self.encoded.strshow() + else: + s += "\n### Already encoded ###\n%r" % self.encoded + s += "\n### Remaining ###\n%r" % self.remaining + return s + + +class OER_Decoding_Error(ASN1_Decoding_Error): + def __init__(self, + msg, # type: str + decoded=None, # type: Optional[Any] + remaining=b"" # type: bytes + ): + # type: (...) -> None + Exception.__init__(self, msg) + self.remaining = remaining + self.decoded = decoded + + def __str__(self): + # type: () -> str + s = Exception.__str__(self) + if isinstance(self.decoded, ASN1_Object): + s += "\n### Already decoded ###\n%s" % self.decoded.strshow() + else: + s += "\n### Already decoded ###\n%r" % self.decoded + s += "\n### Remaining ###\n%r" % self.remaining + return s + + +class OER_BadTag_Decoding_Error(OER_Decoding_Error, + ASN1_BadTag_Decoding_Error): + pass + + +# OER tag classes (bits 8-7 of the first identifier octet) +OER_CLASS_UNIVERSAL = 0x00 +OER_CLASS_APPLICATION = 0x40 +OER_CLASS_CONTEXT = 0x80 +OER_CLASS_PRIVATE = 0xc0 + + +def OER_len_enc(ll): + # type: (int) -> bytes + if ll < 128: + return chb(ll) + encoded = [] + value = ll + while value > 0: + encoded.insert(0, value & 0xff) + value >>= 8 + if len(encoded) > 127: + raise OER_Exception( + "OER_len_enc: Length too long (%i) to be encoded" % len(encoded) + ) + return chb(0x80 | len(encoded)) + bytes(encoded) + + +def OER_len_dec(s): + # type: (bytes) -> Tuple[int, bytes] + if not s: + raise OER_Decoding_Error("OER_len_dec: got empty string", remaining=s) + tmp_len = orb(s[0]) + if not tmp_len & 0x80: + return tmp_len, s[1:] + tmp_len &= 0x7f + if len(s) <= tmp_len: + raise OER_Decoding_Error( + "OER_len_dec: Got %i bytes while expecting %i" % + (len(s) - 1, tmp_len), + remaining=s + ) + ll = 0 + for c in s[1:tmp_len + 1]: + ll <<= 8 + ll |= orb(c) + return ll, s[tmp_len + 1:] + + +def OER_signed_integer_enc(i): + # type: (int) -> bytes + if i < 0: + number_of_bits = i.bit_length() + number_of_bytes = (number_of_bits + 7) // 8 + value = (1 << (8 * number_of_bytes)) + i + if (value & (1 << (8 * number_of_bytes - 1))) == 0: + value |= (0xff << (8 * number_of_bytes)) + number_of_bytes += 1 + elif i > 0: + number_of_bits = i.bit_length() + number_of_bytes = (number_of_bits + 7) // 8 + if number_of_bits == (8 * number_of_bytes): + number_of_bytes += 1 + value = i + else: + number_of_bytes = 1 + value = 0 + return OER_len_enc(number_of_bytes) + value.to_bytes(number_of_bytes, "big") + + +def OER_signed_integer_dec(s): + # type: (bytes) -> Tuple[int, bytes] + number_of_bytes, s = OER_len_dec(s) + if len(s) < number_of_bytes: + raise OER_Decoding_Error( + "OER_signed_integer_dec: Got %i bytes while expecting %i" % + (len(s), number_of_bytes), + remaining=s + ) + value = int.from_bytes(s[:number_of_bytes], "big") + number_of_bits = 8 * number_of_bytes + if value & (1 << (number_of_bits - 1)): + value -= (1 << number_of_bits) - 1 + value -= 1 + return value, s[number_of_bytes:] + + +def OER_unsigned_integer_enc(i): + # type: (int) -> bytes + number_of_bits = max(i.bit_length(), 1) + number_of_bytes = (number_of_bits + 7) // 8 + return OER_len_enc(number_of_bytes) + i.to_bytes(number_of_bytes, "big") + + +def OER_unsigned_integer_dec(s): + # type: (bytes) -> Tuple[int, bytes] + number_of_bytes, s = OER_len_dec(s) + if len(s) < number_of_bytes: + raise OER_Decoding_Error( + "OER_unsigned_integer_dec: Got %i bytes while expecting %i" % + (len(s), number_of_bytes), + remaining=s + ) + value = int.from_bytes(s[:number_of_bytes], "big") + return value, s[number_of_bytes:] + + +def OER_fixed_integer_enc(i, length, signed=True): + # type: (int, int, bool) -> bytes + fmt = {1: ">b", 2: ">h", 4: ">i", 8: ">q"} if signed else { + 1: ">B", 2: ">H", 4: ">I", 8: ">Q" + } + try: + return struct.pack(fmt[length], i) + except KeyError: + raise OER_Encoding_Error( + "OER_fixed_integer_enc: invalid length %i" % length + ) + + +def OER_fixed_integer_dec(s, length, signed=True): + # type: (bytes, int, bool) -> Tuple[int, bytes] + if len(s) < length: + raise OER_Decoding_Error( + "OER_fixed_integer_dec: Got %i bytes while expecting %i" % + (len(s), length), + remaining=s + ) + fmt = {1: ">b", 2: ">h", 4: ">i", 8: ">q"} if signed else { + 1: ">B", 2: ">H", 4: ">I", 8: ">Q" + } + try: + return struct.unpack(fmt[length], s[:length])[0], s[length:] + except KeyError: + raise OER_Decoding_Error( + "OER_fixed_integer_dec: invalid length %i" % length, + remaining=s + ) + + +def OER_enumerated_enc(i): + # type: (int) -> bytes + if 0 <= i <= 127: + return chb(i) + body = OER_signed_integer_enc(i)[1:] + return chb(0x80 | len(body)) + body + + +def OER_enumerated_dec(s): + # type: (bytes) -> Tuple[int, bytes] + if not s: + raise OER_Decoding_Error("OER_enumerated_dec: got empty string", + remaining=s) + first = orb(s[0]) + if not (first & 0x80): + return first, s[1:] + length = first & 0x7f + if len(s) < length + 1: + raise OER_Decoding_Error( + "OER_enumerated_dec: Got %i bytes while expecting %i" % + (len(s) - 1, length), + remaining=s + ) + value = int.from_bytes(s[1:length + 1], "big", signed=True) + return value, s[length + 1:] + + +def OER_tag_enc(n, tag_class=OER_CLASS_CONTEXT): + # type: (int, int) -> bytes + if n < 63: + return chb(tag_class | n) + tag = bytearray([tag_class | 0x3f]) + encoded = [] + value = n + while value > 0: + encoded.append(0x80 | (value & 0x7f)) + value >>= 7 + encoded[0] &= 0x7f + encoded.reverse() + tag.extend(encoded) + return bytes(tag) + + +def OER_tag_dec(s): + # type: (bytes) -> Tuple[int, int, bytes] + if not s: + raise OER_Decoding_Error("OER_tag_dec: got empty string", remaining=s) + first = orb(s[0]) + tag_class = first & 0xc0 + tag_number = first & 0x3f + if tag_number != 0x3f: + return tag_class, tag_number, s[1:] + tag_number = 0 + i = 1 + while i < len(s): + c = orb(s[i]) + tag_number <<= 7 + tag_number |= c & 0x7f + i += 1 + if not (c & 0x80): + break + else: + raise OER_Decoding_Error("OER_tag_dec: unfinished tag", remaining=s) + return tag_class, tag_number, s[i:] + + +def OER_id_dec(s): + # type: (bytes) -> Tuple[int, bytes] + tag_class, tag_number, remainder = OER_tag_dec(s) + return tag_class | tag_number, remainder + + +def OER_tagging_dec(s, # type: bytes + hidden_tag=None, # type: Optional[int | ASN1Tag] + implicit_tag=None, # type: Optional[int] + explicit_tag=None, # type: Optional[int] + safe=False, # type: Optional[bool] + _fname="", # type: str + ): + # type: (...) -> Tuple[Optional[int], bytes] + # OER does not use implicit tagging. Explicit tags are encoded as choice + # alternatives (tag + value). + real_tag = None + if explicit_tag is not None and len(s) > 0: + err_msg = ( + "OER_tagging_dec: observed tag 0x%.02x does not " + "match expected tag 0x%.02x (%s)" + ) + tag_class, tag_number, remainder = OER_tag_dec(s) + observed = tag_class | tag_number + if observed != explicit_tag: + if not safe: + raise OER_Decoding_Error( + err_msg % (observed, explicit_tag, _fname), + remaining=s) + real_tag = observed + s = remainder + return real_tag, s + + +def OER_tagging_enc(s, implicit_tag=None, explicit_tag=None): + # type: (bytes, Optional[int], Optional[int]) -> bytes + if explicit_tag is not None: + return OER_tag_enc(explicit_tag & 0x3f, explicit_tag & 0xc0) + s + return s + + +class OERcodec_metaclass(type): + def __new__(cls, + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> Type['OERcodec_Object[Any]'] + c = cast('Type[OERcodec_Object[Any]]', + super(OERcodec_metaclass, cls).__new__(cls, name, bases, dct)) + try: + c.tag.register(c.codec, c) + except Exception: + warning("Error registering %r for %r" % (c.tag, c.codec)) + return c + + +_K = TypeVar('_K') + + +class OERcodec_Object(Generic[_K], metaclass=OERcodec_metaclass): + codec = ASN1_Codecs.OER + tag = ASN1_Class_UNIVERSAL.ANY + + @classmethod + def asn1_object(cls, val): + # type: (_K) -> ASN1_Object[_K] + return cls.tag.asn1_object(val) + + @classmethod + def check_string(cls, s): + # type: (bytes) -> None + if not s: + raise OER_Decoding_Error( + "%s: Got empty object while expecting %r" % + (cls.__name__, cls.tag), remaining=s + ) + + @classmethod + def check_type(cls, s): + # type: (bytes) -> bytes + cls.check_string(s) + return s + + @classmethod + def check_type_get_len(cls, s): + # type: (bytes) -> Tuple[int, bytes] + cls.check_string(s) + return len(s), s + + @classmethod + def check_type_check_len(cls, s): + # type: (bytes) -> Tuple[int, bytes, bytes] + cls.check_string(s) + return len(s), s, b"" + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[Any], bytes] + raise OER_Decoding_Error( + "%s: Cannot decode unknown OER type without context" % + cls.__name__, remaining=s + ) + + @classmethod + def dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] + if not safe: + return cls.do_dec(s, context, safe, size_len, oer_unsigned) + try: + return cls.do_dec(s, context, safe, size_len, oer_unsigned) + except OER_BadTag_Decoding_Error as e: + o, remain = OERcodec_Object.dec( + e.remaining, context, safe, size_len, oer_unsigned + ) + return ASN1_BADTAG(o), remain + except OER_Decoding_Error as e: + return ASN1_DECODING_ERROR(s, exc=e), b"" + except ASN1_Error as e: + return ASN1_DECODING_ERROR(s, exc=e), b"" + + @classmethod + def safedec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] + return cls.dec( + s, context, safe=True, + size_len=size_len, oer_unsigned=oer_unsigned, + ) + + @classmethod + def enc(cls, s, size_len=0): + # type: (_K, Optional[int]) -> bytes + if isinstance(s, (str, bytes)): + return OERcodec_STRING.enc(s, size_len=size_len) + else: + try: + return OERcodec_INTEGER.enc(int(s), size_len=size_len) # type: ignore + except TypeError: + raise TypeError("Trying to encode an invalid value !") + + +ASN1_Codecs.OER.register_stem(OERcodec_Object) + + +########################## +# OERcodec objects # +########################## + +class OERcodec_INTEGER(OERcodec_Object[int]): + tag = ASN1_Class_UNIVERSAL.INTEGER + + @classmethod + def enc(cls, i, size_len=0): + # type: (int, Optional[int]) -> bytes + if size_len in (1, 2, 4, 8): + if i >= 0: + if size_len == 1 and 0 <= i <= 255: + return OER_fixed_integer_enc(i, 1, signed=False) + if size_len == 2 and 0 <= i <= 65535: + return OER_fixed_integer_enc(i, 2, signed=False) + if size_len == 4 and 0 <= i <= 4294967295: + return OER_fixed_integer_enc(i, 4, signed=False) + if size_len == 8 and 0 <= i <= 18446744073709551615: + return OER_fixed_integer_enc(i, 8, signed=False) + return OER_fixed_integer_enc(i, size_len, signed=True) + return OER_signed_integer_enc(i) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[int], bytes] + if size_len in (1, 2, 4, 8): + x, t = OER_fixed_integer_dec( + s, size_len, signed=not oer_unsigned + ) + return cls.asn1_object(x), t + if oer_unsigned: + x, t = OER_unsigned_integer_dec(s) + else: + x, t = OER_signed_integer_dec(s) + return cls.asn1_object(x), t + + +class OERcodec_BOOLEAN(OERcodec_Object[int]): + tag = ASN1_Class_UNIVERSAL.BOOLEAN + + @classmethod + def enc(cls, i, size_len=0): + # type: (int, Optional[int]) -> bytes + return chb(0xff if i else 0x00) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[int], bytes] + cls.check_string(s) + return cls.asn1_object(0 if orb(s[0]) == 0 else 1), s[1:] + + +class OERcodec_BIT_STRING(OERcodec_Object[str]): + tag = ASN1_Class_UNIVERSAL.BIT_STRING + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[str], bytes] + l, s = OER_len_dec(s) + if l == 0: + return cls.tag.asn1_object(""), s + if len(s) < l: + raise OER_Decoding_Error( + "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), l), + remaining=s + ) + unused_bits = orb(s[0]) + if safe and unused_bits > 7: + raise OER_Decoding_Error( + "OERcodec_BIT_STRING: too many unused_bits advertised", + remaining=s + ) + fs = "".join(binrepr(orb(x)).zfill(8) for x in s[1:l]) + if unused_bits > 0: + fs = fs[:-unused_bits] + return cls.tag.asn1_object(fs), s[l:] + + @classmethod + def enc(cls, _s, size_len=0): + # type: (AnyStr, Optional[int]) -> bytes + s = bytes_encode(_s) + if len(s) % 8 == 0: + unused_bits = 0 + else: + unused_bits = 8 - len(s) % 8 + s += b"0" * unused_bits + data = b"".join(chb(int(b"".join(chb(y) for y in x), 2)) + for x in zip(*[iter(s)] * 8)) + body = chb(unused_bits) + data + return OER_len_enc(len(body)) + body + + +class OERcodec_STRING(OERcodec_Object[str]): + tag = ASN1_Class_UNIVERSAL.STRING + + @classmethod + def enc(cls, _s, size_len=0): + # type: (Union[str, bytes], Optional[int]) -> bytes + s = bytes_encode(_s) + if size_len and size_len == len(s): + return s + return OER_len_enc(len(s)) + s + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[Any], bytes] + if size_len and size_len not in (1, 2, 4, 8): + if len(s) < size_len: + raise OER_Decoding_Error( + "%s: Got %i bytes while expecting %i" % + (cls.__name__, len(s), size_len), + remaining=s + ) + return cls.tag.asn1_object(s[:size_len]), s[size_len:] + l, s = OER_len_dec(s) + if len(s) < l: + raise OER_Decoding_Error( + "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), l), + remaining=s + ) + return cls.tag.asn1_object(s[:l]), s[l:] + + +class OERcodec_NULL(OERcodec_Object[None]): + tag = ASN1_Class_UNIVERSAL.NULL + + @classmethod + def enc(cls, i, size_len=0): + # type: (Any, Optional[int]) -> bytes + return b"" + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[None], bytes] + return cls.asn1_object(None), s + + +class OERcodec_OID(OERcodec_Object[bytes]): + tag = ASN1_Class_UNIVERSAL.OID + + @classmethod + def enc(cls, _oid, size_len=0): + # type: (AnyStr, Optional[int]) -> bytes + oid = bytes_encode(_oid) + if oid: + lst = [int(x) for x in oid.strip(b".").split(b".")] + else: + lst = list() + if len(lst) >= 2: + lst[1] += 40 * lst[0] + del lst[0] + body = b"".join(BER_num_enc(k) for k in lst) + return OER_len_enc(len(body)) + body + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[bytes], bytes] + l, s = OER_len_dec(s) + if len(s) < l: + raise OER_Decoding_Error( + "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), l), + remaining=s + ) + content, t = s[:l], s[l:] + lst = [] + while content: + val, content = BER_num_dec(content) + lst.append(val) + if len(lst) > 0: + lst.insert(0, lst[0] // 40) + lst[1] %= 40 + return ( + cls.asn1_object(b".".join(str(k).encode('ascii') for k in lst)), + t, + ) + + +class OERcodec_ENUMERATED(OERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.ENUMERATED + + @classmethod + def enc(cls, i, size_len=0): + # type: (int, Optional[int]) -> bytes + return OER_enumerated_enc(i) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[int], bytes] + x, t = OER_enumerated_dec(s) + return cls.asn1_object(x), t + + +class OERcodec_UTF8_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.UTF8_STRING + + +class OERcodec_NUMERIC_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.NUMERIC_STRING + + +class OERcodec_PRINTABLE_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.PRINTABLE_STRING + + +class OERcodec_T61_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.T61_STRING + + +class OERcodec_VIDEOTEX_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.VIDEOTEX_STRING + + +class OERcodec_IA5_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.IA5_STRING + + +class OERcodec_GENERAL_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.GENERAL_STRING + + +class OERcodec_UTC_TIME(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.UTC_TIME + + +class OERcodec_GENERALIZED_TIME(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.GENERALIZED_TIME + + +class OERcodec_ISO646_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.ISO646_STRING + + +class OERcodec_UNIVERSAL_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.UNIVERSAL_STRING + + +class OERcodec_BMP_STRING(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.BMP_STRING + + +class OERcodec_SEQUENCE(OERcodec_Object[Union[bytes, List['OERcodec_Object[Any]']]]): + tag = ASN1_Class_UNIVERSAL.SEQUENCE + + @classmethod + def enc(cls, _ll, size_len=0): + # type: (Union[bytes, List[OERcodec_Object[Any]]], Optional[int]) -> bytes + if isinstance(_ll, bytes): + return _ll + return b"".join(x.enc(cls.codec) for x in _ll) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[Union[bytes, List[Any]]], bytes] + raise OER_Decoding_Error( + "OERcodec_SEQUENCE: decoding requires schema-defined field order", + remaining=s + ) + + +class OERcodec_SET(OERcodec_SEQUENCE): + tag = ASN1_Class_UNIVERSAL.SET + + +class OERcodec_IPADDRESS(OERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.IPADDRESS + + @classmethod + def enc(cls, ipaddr_ascii, size_len=0): # type: ignore + # type: (str, Optional[int]) -> bytes + try: + s = inet_aton(ipaddr_ascii) + except Exception: + raise OER_Encoding_Error("IPv4 address could not be encoded") + if size_len == len(s): + return s + return OER_len_enc(len(s)) + s + + @classmethod + def do_dec(cls, s, context=None, safe=False, + size_len=0, oer_unsigned=False): + # type: (bytes, Optional[Any], bool, Optional[int], bool) -> Tuple[ASN1_Object[str], bytes] # noqa: E501 + if size_len == 4: + raw, remain = s[:4], s[4:] + else: + l, remain = OER_len_dec(s) + if len(remain) < l: + raise OER_Decoding_Error("IP address could not be decoded", + remaining=s) + raw, remain = remain[:l], remain[l:] + try: + ipaddr_ascii = inet_ntoa(raw) + except Exception: + raise OER_Decoding_Error("IP address could not be decoded", + remaining=s) + return cls.asn1_object(ipaddr_ascii), remain + + +class OERcodec_COUNTER32(OERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.COUNTER32 + + +class OERcodec_COUNTER64(OERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.COUNTER64 + + +class OERcodec_GAUGE32(OERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.GAUGE32 + + +class OERcodec_TIME_TICKS(OERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.TIME_TICKS diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 9a5284bddfc..a6dd27c9316 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -17,6 +17,8 @@ ASN1_BOOLEAN, ASN1_Class, ASN1_Class_UNIVERSAL, + ASN1_Codecs, + ASN1_Decoding_Error, ASN1_Error, ASN1_INTEGER, ASN1_NULL, @@ -30,6 +32,15 @@ BER_tagging_dec, BER_tagging_enc, ) +from scapy.asn1.oer import ( + OER_Decoding_Error, + OER_id_dec, + OER_tag_enc, + OER_tagging_dec, + OER_tagging_enc, + OER_unsigned_integer_dec, + OER_unsigned_integer_enc, +) from scapy.base_classes import BasePacket from scapy.volatile import ( GeneralizedTime, @@ -93,6 +104,7 @@ def __init__(self, explicit_tag=None, # type: Optional[int] flexible_tag=False, # type: Optional[bool] size_len=None, # type: Optional[int] + oer_unsigned=False, # type: Optional[bool] ): # type: (...) -> None if context is not None: @@ -105,6 +117,7 @@ def __init__(self, else: self.default = self.ASN1_tag.asn1_object(default) # type: ignore self.size_len = size_len + self.oer_unsigned = oer_unsigned self.flexible_tag = flexible_tag if (implicit_tag is not None) and (explicit_tag is not None): err_msg = "field cannot be both implicitly and explicitly tagged" @@ -141,11 +154,18 @@ def m2i(self, pkt, s): as expected or not. Noticeably, input methods from cert.py expect certain exceptions to be raised. Hence default flexible_tag is False. """ - diff_tag, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, - implicit_tag=self.implicit_tag, - explicit_tag=self.explicit_tag, - safe=self.flexible_tag, - _fname=self.name) + if pkt.ASN1_codec == ASN1_Codecs.OER: + diff_tag, s = OER_tagging_dec(s, hidden_tag=self.ASN1_tag, + implicit_tag=self.implicit_tag, + explicit_tag=self.explicit_tag, + safe=self.flexible_tag, + _fname=self.name) + else: + diff_tag, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, + implicit_tag=self.implicit_tag, + explicit_tag=self.explicit_tag, + safe=self.flexible_tag, + _fname=self.name) if diff_tag is not None: # this implies that flexible_tag was True if self.implicit_tag is not None: @@ -153,10 +173,16 @@ def m2i(self, pkt, s): elif self.explicit_tag is not None: self.explicit_tag = diff_tag codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + oer_kwargs = {} # type: Dict[str, Any] + if pkt.ASN1_codec == ASN1_Codecs.OER: + oer_kwargs = { + "size_len": self.size_len or 0, + "oer_unsigned": self.oer_unsigned, + } if self.flexible_tag: - return codec.safedec(s, context=self.context) # type: ignore + return codec.safedec(s, context=self.context, **oer_kwargs) # type: ignore else: - return codec.dec(s, context=self.context) # type: ignore + return codec.dec(s, context=self.context, **oer_kwargs) # type: ignore def i2m(self, pkt, x): # type: (ASN1_Packet, Union[bytes, _I, _A]) -> bytes @@ -172,6 +198,12 @@ def i2m(self, pkt, x): raise ASN1_Error("Encoding Error: got %r instead of an %r for field [%s]" % (x, self.ASN1_tag, self.name)) # noqa: E501 else: s = self.ASN1_tag.get_codec(pkt.ASN1_codec).enc(x, size_len=self.size_len) + if pkt.ASN1_codec == ASN1_Codecs.OER: + return cast(bytes, OER_tagging_enc( + s, + implicit_tag=self.implicit_tag, + explicit_tag=self.explicit_tag, + )) return BER_tagging_enc(s, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag) @@ -244,6 +276,18 @@ def copy(self): # type: () -> ASN1F_field[_I, _A] return copy.copy(self) + def _encode_item(self, pkt, item): + # type: (ASN1_Packet, Any) -> bytes + if isinstance(item, ASN1_Object): + return item.enc(pkt.ASN1_codec) + if hasattr(item, "self_build"): + return cast("ASN1_Packet", item).self_build() + codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + size_len = self.size_len or 0 + if pkt.ASN1_codec == ASN1_Codecs.OER and self.oer_unsigned: + return codec.enc(item, size_len=size_len, oer_unsigned=True) # type: ignore[call-arg] + return codec.enc(item, size_len=size_len) + ############################ # Simple ASN1 Fields # @@ -471,6 +515,29 @@ def m2i(self, pkt, s): Thus m2i returns an empty list (along with the proper remainder). It is discarded by dissect() and should not be missed elsewhere. """ + if pkt.ASN1_codec == ASN1_Codecs.OER: + diff_tag, s = OER_tagging_dec(s, hidden_tag=self.ASN1_tag, + implicit_tag=self.implicit_tag, + explicit_tag=self.explicit_tag, + safe=self.flexible_tag, + _fname=pkt.name) + if diff_tag is not None: + if self.implicit_tag is not None: + self.implicit_tag = diff_tag + elif self.explicit_tag is not None: + self.explicit_tag = diff_tag + if len(s) == 0: + for obj in self.seq: + obj.set_val(pkt, None) + else: + for obj in self.seq: + try: + s = obj.dissect(pkt, s) + except ASN1F_badsequence: + break + if len(s) > 0: + raise OER_Decoding_Error("unexpected remainder", remaining=s) + return [], b"" diff_tag, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag, @@ -569,6 +636,25 @@ def m2i(self, s, # type: bytes ): # type: (...) -> Tuple[List[Any], bytes] + if pkt.ASN1_codec == ASN1_Codecs.OER: + diff_tag, s = OER_tagging_dec(s, hidden_tag=self.ASN1_tag, + implicit_tag=self.implicit_tag, + explicit_tag=self.explicit_tag, + safe=self.flexible_tag) + if diff_tag is not None: + if self.implicit_tag is not None: + self.implicit_tag = diff_tag + elif self.explicit_tag is not None: + self.explicit_tag = diff_tag + count, s = OER_unsigned_integer_dec(s) + lst = [] + for _ in range(count): + c, s = self._extract_packet(s, pkt) # type: ignore + if c: + lst.append(c) + if len(s) > 0: + raise OER_Decoding_Error("unexpected remainder", remaining=s) + return lst, b"" diff_tag, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag, @@ -597,8 +683,12 @@ def build(self, pkt): s = cast(Union[List[_SEQ_T], bytes], val) elif val is None: s = b"" + if pkt.ASN1_codec == ASN1_Codecs.OER: + s = OER_unsigned_integer_enc(0) else: - s = b"".join(bytes(i) for i in val) + s = b"".join(self.fld._encode_item(pkt, i) for i in val) + if pkt.ASN1_codec == ASN1_Codecs.OER: + s = OER_unsigned_integer_enc(len(val)) + s return self.i2m(pkt, s) def i2repr(self, pkt, x): @@ -657,7 +747,7 @@ def m2i(self, pkt, s): # type: (ASN1_Packet, bytes) -> Tuple[Any, bytes] try: return self._field.m2i(pkt, s) - except (ASN1_Error, ASN1F_badsequence, BER_Decoding_Error): + except (ASN1_Error, ASN1F_badsequence, ASN1_Decoding_Error): # ASN1_Error may be raised by ASN1F_CHOICE return None, s @@ -665,7 +755,7 @@ def dissect(self, pkt, s): # type: (ASN1_Packet, bytes) -> bytes try: return self._field.dissect(pkt, s) - except (ASN1_Error, ASN1F_badsequence, BER_Decoding_Error): + except (ASN1_Error, ASN1F_badsequence, ASN1_Decoding_Error): self._field.set_val(pkt, None) return s @@ -756,9 +846,15 @@ def m2i(self, pkt, s): """ if len(s) == 0: raise ASN1_Error("ASN1F_CHOICE: got empty string") - _, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, - explicit_tag=self.explicit_tag) - tag, _ = BER_id_dec(s) + if pkt.ASN1_codec == ASN1_Codecs.OER: + _, s = OER_tagging_dec(s, hidden_tag=self.ASN1_tag, + explicit_tag=self.explicit_tag) + tag, payload = OER_id_dec(s) + else: + _, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, + explicit_tag=self.explicit_tag) + tag, _ = BER_id_dec(s) + payload = s if tag in self.choices: choice = self.choices[tag] else: @@ -773,24 +869,49 @@ def m2i(self, pkt, s): ) if hasattr(choice, "ASN1_root"): # we don't want to import ASN1_Packet in this module... - return self.extract_packet(choice, s, _underlayer=pkt) # type: ignore + return self.extract_packet(choice, payload, _underlayer=pkt) # type: ignore elif isinstance(choice, type): - return choice(self.name, b"").m2i(pkt, s) + return choice(self.name, b"").m2i(pkt, payload) else: # XXX check properly if this is an ASN1F_PACKET - return choice.m2i(pkt, s) + return choice.m2i(pkt, payload) + + def _choice_tag_for(self, x): + # type: (Any) -> Optional[int] + for tag, choice in self.choices.items(): + if hasattr(choice, "ASN1_root"): + if isinstance(x, cast("Type[ASN1_Packet]", choice)): + return tag + elif isinstance(choice, type) and hasattr(choice, "ASN1_tag"): + if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: + return tag + elif hasattr(choice, "ASN1_tag"): + if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: + return tag + return None def i2m(self, pkt, x): # type: (ASN1_Packet, Any) -> bytes if x is None: s = b"" else: - s = bytes(x) - if hash(type(x)) in self.pktchoices: + if isinstance(x, ASN1_Object): + s = x.enc(pkt.ASN1_codec) + elif hasattr(x, "self_build"): + s = cast("ASN1_Packet", x).self_build() + else: + s = bytes(x) + if pkt.ASN1_codec == ASN1_Codecs.OER: + alt_tag = self._choice_tag_for(x) + if alt_tag is not None: + s = OER_tag_enc(alt_tag & 0x3f, alt_tag & 0xc0) + s + elif hash(type(x)) in self.pktchoices: imp, exp = self.pktchoices[hash(type(x))] s = BER_tagging_enc(s, implicit_tag=imp, explicit_tag=exp) + if pkt.ASN1_codec == ASN1_Codecs.OER: + return cast(bytes, OER_tagging_enc(s, explicit_tag=self.explicit_tag)) return BER_tagging_enc(s, explicit_tag=self.explicit_tag) def randval(self): diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index 9fa0bad0f44..aa1fc734f95 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -101,3 +101,157 @@ ASN1_UTC_TIME(datetime(2020, 12, 31)).val == "201231000000" ASN1_UTC_TIME(datetime(2020, 12, 31, tzinfo=timezone.utc)).val == "201231000000Z" = UTC datetime construction (offset) ASN1_UTC_TIME(datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-23, minutes=-59)))).val == "201231000000-2359" + ++ ASN.1 OER codec += OER length determinant short form +OER_len_enc(3) == b"\x03" += OER length determinant long form +OER_len_enc(200) == b"\x81\xc8" += OER boolean false +OERcodec_BOOLEAN.enc(0) == b"\x00" += OER boolean true +OERcodec_BOOLEAN.enc(1) == b"\xff" += OER null +OERcodec_NULL.enc(None) == b"" += OER unconstrained integer +OERcodec_INTEGER.enc(4) == b"\x01\x04" += OER constrained unsigned integer +OERcodec_INTEGER.enc(4, size_len=1) == b"\x04" += OER constrained signed integer +OERcodec_INTEGER.enc(4, size_len=2) == b"\x00\x04" += OER enumerated short form +OERcodec_ENUMERATED.enc(6) == b"\x06" += OER octet string +OERcodec_STRING.enc(b"ABC") == b"\x03ABC" += OER OID +OERcodec_OID.enc("1.2.3") == b"\x02\x2a\x03" += OER integer roundtrip +x, r = OERcodec_INTEGER.do_dec(OERcodec_INTEGER.enc(12345)) +x.val == 12345 and r == b"" += OER boolean roundtrip +x, r = OERcodec_BOOLEAN.do_dec(OERcodec_BOOLEAN.enc(1)) +x.val == 1 and r == b"" += OER ASN1 object encoding +ASN1_INTEGER(42).enc(ASN1_Codecs.OER) == b"\x01*" += OER codec registration +ASN1_Class_UNIVERSAL.INTEGER.get_codec(ASN1_Codecs.OER) is OERcodec_INTEGER + ++ ASN.1 OER codec (extended) += OER length zero +OER_len_enc(0) == b"\x00" += OER length boundary short form +OER_len_enc(127) == b"\x7f" += OER length boundary long form +OER_len_enc(128) == b"\x81\x80" += OER length roundtrip +l, r = OER_len_dec(OER_len_enc(999)) +l == 999 and r == b"" += OER signed integer zero +OER_signed_integer_enc(0) == b"\x01\x00" += OER signed integer negative +OER_signed_integer_enc(-255) == b"\x02\xff\x01" += OER signed integer large +OER_signed_integer_enc(100000) == b"\x03\x01\x86\xa0" += OER signed integer roundtrip +v, r = OER_signed_integer_dec(OER_signed_integer_enc(-1234567)) +v == -1234567 and r == b"" += OER unsigned integer zero +OER_unsigned_integer_enc(0) == b"\x01\x00" += OER unsigned integer roundtrip +v, r = OER_unsigned_integer_dec(OER_unsigned_integer_enc(65535)) +v == 65535 and r == b"" += OER fixed unsigned 1 byte +OERcodec_INTEGER.enc(255, size_len=1) == b"\xff" += OER fixed signed 2 bytes negative +OERcodec_INTEGER.enc(-2, size_len=2) == b"\xff\xfe" += OER fixed signed 4 bytes +OERcodec_INTEGER.enc(-2, size_len=4) == b"\xff\xff\xff\xfe" += OER enumerated long form +OERcodec_ENUMERATED.enc(128) == b"\x82\x00\x80" += OER enumerated negative +OERcodec_ENUMERATED.enc(-1) == b"\x81\xff" += OER enumerated roundtrip +x, r = OERcodec_ENUMERATED.do_dec(OERcodec_ENUMERATED.enc(128)) +x.val == 128 and r == b"" += OER null roundtrip +x, r = OERcodec_NULL.do_dec(OERcodec_NULL.enc(None)) +x.val is None and r == b"" += OER octet string empty +OERcodec_STRING.enc(b"") == b"\x00" += OER octet string fixed size +OERcodec_STRING.enc(b"\x12\x34\x56", size_len=3) == b"\x12\x34\x56" += OER octet string roundtrip +x, r = OERcodec_STRING.do_dec(OERcodec_STRING.enc(b"\x12\x34")) +x.val == b"\x12\x34" and r == b"" += OER OID 1.2 +OERcodec_OID.enc("1.2") == b"\x01\x2a" += OER OID roundtrip +x, r = OERcodec_OID.do_dec(OERcodec_OID.enc("1.2.3321")) +x.val == "1.2.3321" and r == b"" += OER bit string variable size +OERcodec_BIT_STRING.enc("0100") == b"\x02\x04\x40" += OER bit string roundtrip +x, r = OERcodec_BIT_STRING.do_dec(OERcodec_BIT_STRING.enc("01000001")) +x.val == "01000001" and r == b"" += OER IA5 string +OERcodec_IA5_STRING.enc(b"ABC") == b"\x03ABC" += OER tag short form +OER_tag_enc(1, OER_CLASS_CONTEXT) == b"\x81" += OER tag roundtrip +cls, num, r = OER_tag_dec(OER_tag_enc(1, OER_CLASS_CONTEXT)) +cls == OER_CLASS_CONTEXT and num == 1 and r == b"" += OER sequence concat +OERcodec_SEQUENCE.enc([ASN1_INTEGER(4), ASN1_INTEGER(5)]) == b"\x01\x04\x01\x05" += OER ASN1 boolean object +ASN1_BOOLEAN(1).enc(ASN1_Codecs.OER) == b"\xff" += OER ASN1 null object +ASN1_NULL(None).enc(ASN1_Codecs.OER) == b"" + ++ ASN.1 OER interoperability (asn1tools) += asn1tools availability probe +from test.scapy.layers.oer_iop import require_asn1tools +HAS_ASN1TOOLS_OER = require_asn1tools() += primitive encode interop with asn1tools +(not HAS_ASN1TOOLS_OER) or __import__('test.scapy.layers.oer_iop', fromlist=['check_primitive_interop']).check_primitive_interop() += scapy encode asn1tools decode +(not HAS_ASN1TOOLS_OER) or __import__('test.scapy.layers.oer_iop', fromlist=['check_asn1tools_decode_scapy_encode']).check_asn1tools_decode_scapy_encode() + ++ ASN.1 OER review fixes += OER fixed integer decode roundtrip +x, r = OERcodec_INTEGER.do_dec(OERcodec_INTEGER.enc(128, size_len=1), size_len=1, oer_unsigned=True) +x.val == 128 and r == b"" += OER fixed integer signed decode +x, r = OERcodec_INTEGER.do_dec(OERcodec_INTEGER.enc(-2, size_len=2), size_len=2) +x.val == -2 and r == b"" += OER fixed octet string decode +x, r = OERcodec_STRING.do_dec(OERcodec_STRING.enc(b"\x12\x34\x56", size_len=3), size_len=3) +x.val == b"\x12\x34\x56" and r == b"" += OER explicit null tagging +OER_tagging_enc(OERcodec_NULL.enc(None), explicit_tag=0x81) == b"\x81" += OER choice id decode +tag, r = OER_id_dec(b"\x81\x01") +tag == 0x81 and r == b"\x01" + ++ ASN.1 OER fuzzing += OER fuzz encode +__import__('test.scapy.layers.oer_fuzz', fromlist=['check_oer_fuzz_encode']).check_oer_fuzz_encode() += OER fuzz encode roundtrip +__import__('test.scapy.layers.oer_fuzz', fromlist=['check_oer_fuzz_roundtrip']).check_oer_fuzz_roundtrip() += OER fuzz codec decode +__import__('test.scapy.layers.oer_fuzz', fromlist=['check_oer_fuzz_codec_decode']).check_oer_fuzz_codec_decode() += OER fuzz packet decode +__import__('test.scapy.layers.oer_fuzz', fromlist=['check_oer_fuzz_packet_decode']).check_oer_fuzz_packet_decode() + ++ ASN.1 OER packets and fields += OER field explicit tag +__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_field_explicit_tag']).check_oer_field_explicit_tag() += OER field fixed size +__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_field_fixed_size']).check_oer_field_fixed_size() += OER field optional +__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_field_optional']).check_oer_field_optional() += OER field sequence of +__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_field_sequence_of']).check_oer_field_sequence_of() += OER field choice +__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_field_choice']).check_oer_field_choice() += OER packet record +__import__('test.scapy.layers.oer_packets', fromlist=['check_oer_packet_record']).check_oer_packet_record() diff --git a/test/scapy/layers/oer_fuzz.py b/test/scapy/layers/oer_fuzz.py new file mode 100644 index 00000000000..a322a88aab3 --- /dev/null +++ b/test/scapy/layers/oer_fuzz.py @@ -0,0 +1,106 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +OER fuzzing helpers. + +Exercise OER encode/decode paths with packet.fuzz() and random payloads. +""" + +import os +import random +from typing import Iterable, Type + +from scapy.asn1.asn1 import ASN1_Codecs, ASN1_Decoding_Error, ASN1_Error +from scapy.asn1.oer import ( + OER_Decoding_Error, + OERcodec_BIT_STRING, + OERcodec_BOOLEAN, + OERcodec_ENUMERATED, + OERcodec_INTEGER, + OERcodec_NULL, + OERcodec_OID, + OERcodec_STRING, +) +from scapy.asn1fields import ( + ASN1F_BOOLEAN, + ASN1F_INTEGER, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, + ASN1F_STRING, + ASN1F_optional, +) +from scapy.asn1packet import ASN1_Packet +from scapy.packet import fuzz, raw + +_OER_CODEC_CLASSES = ( + OERcodec_INTEGER, + OERcodec_BOOLEAN, + OERcodec_NULL, + OERcodec_STRING, + OERcodec_OID, + OERcodec_ENUMERATED, + OERcodec_BIT_STRING, +) + +_DECODE_ERRORS = ( + OER_Decoding_Error, + ASN1_Decoding_Error, + ASN1_Error, + ValueError, + IndexError, +) + + +class OERFuzzRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_STRING("label", ""), + ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ) + + +def _fuzz_packets(): + # type: () -> Iterable[Type[ASN1_Packet]] + return (OERFuzzRecord,) + + +def check_oer_fuzz_encode(iterations=25): + # type: (int) -> None + for cls in _fuzz_packets(): + for _ in range(iterations): + data = raw(fuzz(cls())) + assert isinstance(data, bytes) + + +def check_oer_fuzz_roundtrip(iterations=25): + # type: (int) -> None + for cls in _fuzz_packets(): + for _ in range(iterations): + cls(raw(fuzz(cls()))) + + +def check_oer_fuzz_codec_decode(iterations=100): + # type: (int) -> None + for codec in _OER_CODEC_CLASSES: + for _ in range(iterations): + data = os.urandom(random.randint(0, 64)) + try: + codec.safedec(data) + except _DECODE_ERRORS: + pass + + +def check_oer_fuzz_packet_decode(iterations=100): + # type: (int) -> None + for cls in _fuzz_packets(): + for _ in range(iterations): + data = os.urandom(random.randint(0, 128)) + try: + cls(data) + except _DECODE_ERRORS: + pass diff --git a/test/scapy/layers/oer_iop.py b/test/scapy/layers/oer_iop.py new file mode 100644 index 00000000000..48088153047 --- /dev/null +++ b/test/scapy/layers/oer_iop.py @@ -0,0 +1,231 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +OER interoperability helpers. + +Cross-check Scapy's OER codec against asn1tools when available. +asn1tools is an optional test dependency (pip install asn1tools). +Reference vectors are taken from asn1tools/tests/test_oer.py. +""" + +from typing import Any, Callable, List, Optional, Tuple + +try: + import asn1tools + HAS_ASN1TOOLS = True +except ImportError: + asn1tools = None # type: ignore + HAS_ASN1TOOLS = False + +from scapy.asn1.oer import ( + OERcodec_BIT_STRING, + OERcodec_BOOLEAN, + OERcodec_ENUMERATED, + OERcodec_INTEGER, + OERcodec_NULL, + OERcodec_OID, + OERcodec_STRING, + OER_len_dec, + OER_len_enc, + OER_signed_integer_dec, + OER_signed_integer_enc, + OER_unsigned_integer_dec, + OER_unsigned_integer_enc, +) + + +ASN1TOOLS_INTEGER_SPEC = ( + "Foo DEFINITIONS AUTOMATIC TAGS ::= " + "BEGIN " + "A ::= INTEGER " + "B ::= INTEGER (-128..127) " + "C ::= INTEGER (-32768..32767) " + "D ::= INTEGER (-2147483648..2147483647) " + "E ::= INTEGER (-9223372036854775808..9223372036854775807) " + "F ::= INTEGER (0..255) " + "G ::= INTEGER (0..65535) " + "H ::= INTEGER (0..4294967295) " + "I ::= INTEGER (0..18446744073709551615) " + "J ::= INTEGER (0..18446744073709551616) " + "K ::= INTEGER (1..MAX) " + "L ::= INTEGER (MIN..0) " + "END" +) + +# (asn1tools type, value, scapy encoder callable) +INTEGER_VECTORS = [ + ("A", 0, lambda v: OERcodec_INTEGER.enc(v)), + ("A", 128, lambda v: OERcodec_INTEGER.enc(v)), + ("A", 100000, lambda v: OERcodec_INTEGER.enc(v)), + ("A", -255, lambda v: OERcodec_INTEGER.enc(v)), + ("A", -1234567, lambda v: OERcodec_INTEGER.enc(v)), + ("B", -2, lambda v: OERcodec_INTEGER.enc(v, size_len=1)), + ("C", -2, lambda v: OERcodec_INTEGER.enc(v, size_len=2)), + ("D", -2, lambda v: OERcodec_INTEGER.enc(v, size_len=4)), + ("E", -2, lambda v: OERcodec_INTEGER.enc(v, size_len=8)), + ("F", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=1)), + ("G", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=2)), + ("G", 1000, lambda v: OERcodec_INTEGER.enc(v, size_len=2)), + ("H", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=4)), + ("I", 128, lambda v: OERcodec_INTEGER.enc(v, size_len=8)), + ("B", 1, lambda v: OERcodec_INTEGER.enc(v, size_len=1)), + ("K", 1, lambda v: OER_unsigned_integer_enc(v)), + ("K", 128, lambda v: OER_unsigned_integer_enc(v)), + ("L", -128, lambda v: OER_signed_integer_enc(v)), +] + +BOOLEAN_VECTORS = [ + (True, lambda v: OERcodec_BOOLEAN.enc(1 if v else 0)), + (False, lambda v: OERcodec_BOOLEAN.enc(1 if v else 0)), +] + +ENUMERATED_VECTORS = [ + ("A ::= ENUMERATED { a(1) }", "A", "a", 1), + ("B ::= ENUMERATED { a(128) }", "B", "a", 128), + ("C ::= ENUMERATED { a(0), b(127) }", "C", "a", 0), + ("C ::= ENUMERATED { a(0), b(127) }", "C", "b", 127), + ("E ::= ENUMERATED { a(-1), b(1234) }", "E", "a", -1), +] + +OID_VECTORS = [ + ("1.2", lambda v: OERcodec_OID.enc(v)), + ("1.2.3321", lambda v: OERcodec_OID.enc(v)), +] + +OCTET_STRING_VECTORS = [ + (b"\x12\x34", 0), + (b"\x12\x34\x56", 3), +] + +BIT_STRING_VECTORS = [ + # (asn1 value, scapy bit string) + ((b"\x40", 4), "0100"), + ((b"\x41", 8), "01000001"), +] + + +def require_asn1tools(): + # type: () -> bool + """Return True if asn1tools is available for interoperability tests.""" + return HAS_ASN1TOOLS + + +def _compile(spec_body): + # type: (str) -> Any + assert asn1tools is not None + spec = "Foo DEFINITIONS AUTOMATIC TAGS ::= BEGIN %s END" % spec_body + return asn1tools.compile_string(spec, "oer") + + +def _assert_encode_match(spec_body, type_name, value, scapy_enc): + # type: (str, str, Any, Callable[..., bytes]) -> None + compiled = _compile(spec_body) + expected = compiled.encode(type_name, value) + got = scapy_enc(value) + assert got == expected, ( + "OER encode mismatch for %s=%r: asn1tools=%r scapy=%r" % + (type_name, value, expected, got) + ) + + +def _assert_decode_match(type_name, encoded, scapy_dec, expected_value): + # type: (str, bytes, Callable[[bytes], Tuple[Any, bytes]], Any) -> None + obj, remain = scapy_dec(encoded) + assert remain == b"", "unexpected remainder after decode of %s" % type_name + assert obj.val == expected_value, ( + "OER decode mismatch for %s: got %r expected %r" % + (type_name, obj.val, expected_value) + ) + + +def check_primitive_interop(): + # type: () -> bool + """Compare Scapy OER primitives against asn1tools. Returns True on success.""" + if not HAS_ASN1TOOLS: + return True + + compiled = asn1tools.compile_string(ASN1TOOLS_INTEGER_SPEC, "oer") + for type_name, value, enc in INTEGER_VECTORS: + expected = compiled.encode(type_name, value) + got = enc(value) + assert got == expected, ( + "integer %s=%r: asn1tools=%r scapy=%r" % + (type_name, value, expected, got) + ) + if type_name == "A": + dec, remain = OERcodec_INTEGER.do_dec(got) + assert remain == b"" and dec.val == value + + bool_spec = _compile("A ::= BOOLEAN") + for value, enc in BOOLEAN_VECTORS: + expected = bool_spec.encode("A", value) + got = enc(value) + assert got == expected + dec, remain = OERcodec_BOOLEAN.do_dec(got) + assert remain == b"" and dec.val == (1 if value else 0) + + _assert_encode_match("A ::= NULL", "A", None, lambda _: OERcodec_NULL.enc(None)) + + for spec_body, type_name, enum_name, enum_val in ENUMERATED_VECTORS: + compiled = _compile(spec_body) + expected = compiled.encode(type_name, enum_name) + got = OERcodec_ENUMERATED.enc(enum_val) + assert got == expected + dec, remain = OERcodec_ENUMERATED.do_dec(got) + assert remain == b"" and dec.val == enum_val + + oid_spec = _compile("A ::= OBJECT IDENTIFIER") + for oid, enc in OID_VECTORS: + expected = oid_spec.encode("A", oid) + got = enc(oid) + assert got == expected + dec, remain = OERcodec_OID.do_dec(got) + assert remain == b"" and dec.val == oid + + octet_spec = _compile( + "A ::= OCTET STRING\nB ::= OCTET STRING (SIZE (3))" + ) + for data, fixed_size in OCTET_STRING_VECTORS: + type_name = "B" if fixed_size else "A" + expected = octet_spec.encode(type_name, data) + got = OERcodec_STRING.enc(data, size_len=fixed_size or 0) + assert got == expected + dec, remain = OERcodec_STRING.do_dec(got, size_len=fixed_size or 0) + assert remain == b"" and dec.val == data + + bit_spec = _compile("A ::= BIT STRING") + for (data, nbits), bitstr in BIT_STRING_VECTORS: + expected = bit_spec.encode("A", (data, nbits)) + got = OERcodec_BIT_STRING.enc(bitstr) + assert got == expected + dec, remain = OERcodec_BIT_STRING.do_dec(got) + assert remain == b"" and dec.val == bitstr + + return True + + +def check_asn1tools_decode_scapy_encode(): + # type: () -> bool + """Encode with Scapy, decode with asn1tools.""" + if not HAS_ASN1TOOLS: + return True + + compiled = asn1tools.compile_string(ASN1TOOLS_INTEGER_SPEC, "oer") + samples = [("A", 42), ("F", 200), ("B", -99)] + for type_name, value in samples: + encoded = { + "A": OERcodec_INTEGER.enc, + "F": lambda v: OERcodec_INTEGER.enc(v, size_len=1), + "B": lambda v: OERcodec_INTEGER.enc(v, size_len=1), + }[type_name](value) + decoded = compiled.decode(type_name, encoded) + assert decoded == value + + bool_spec = _compile("A ::= BOOLEAN") + for val in [0, 1]: + encoded = OERcodec_BOOLEAN.enc(val) + assert bool_spec.decode("A", encoded) == bool(val) + + return True diff --git a/test/scapy/layers/oer_packets.py b/test/scapy/layers/oer_packets.py new file mode 100644 index 00000000000..515b0209af2 --- /dev/null +++ b/test/scapy/layers/oer_packets.py @@ -0,0 +1,149 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +OER ASN1_Packet and ASN1F_field tests. +""" + +from scapy.asn1.asn1 import ASN1_Codecs, ASN1_INTEGER, ASN1_STRING +from scapy.asn1fields import ( + ASN1F_BOOLEAN, + ASN1F_CHOICE, + ASN1F_INTEGER, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, + ASN1F_STRING, + ASN1F_optional, +) +from scapy.asn1packet import ASN1_Packet +from scapy.packet import raw + + +class OERTaggedInteger(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_INTEGER("n", 0, explicit_tag=0xA1) + + +class OERFixedFields(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), + ASN1F_STRING("s", "", size_len=3), + ) + + +class OEROptionalField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), + ) + + +class OERSequenceOfIntegers(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + + +class OERChoiceField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + +class OERRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_STRING("label", ""), + ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ) + + +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) + + +def check_oer_field_explicit_tag(): + # type: () -> None + pkt = OERTaggedInteger(n=5) + assert raw(pkt) == b"\xa1\x01\x05" + decoded = _roundtrip(OERTaggedInteger, pkt) + assert decoded.n.val == 5 + + +def check_oer_field_fixed_size(): + # type: () -> None + pkt = OERFixedFields(n=200, s=b"ABC") + assert raw(pkt) == b"\xc8ABC" + decoded = _roundtrip(OERFixedFields, pkt) + assert decoded.n.val == 200 + assert decoded.s.val == b"ABC" + + +def check_oer_field_optional(): + # type: () -> None + present = OEROptionalField(id=1, extra=7) + assert raw(present) == b"\x01\x01\xa0\x01\x07" + decoded = _roundtrip(OEROptionalField, present) + assert decoded.id.val == 1 + assert decoded.extra.val == 7 + + absent = OEROptionalField(id=1, extra=None) + assert raw(absent) == b"\x01\x01" + decoded = _roundtrip(OEROptionalField, absent) + assert decoded.id.val == 1 + assert decoded.extra is None + + +def check_oer_field_sequence_of(): + # type: () -> None + pkt = OERSequenceOfIntegers(values=[1, 2, 3]) + assert raw(pkt) == b"\x01\x03\x01\x01\x01\x02\x01\x03" + decoded = _roundtrip(OERSequenceOfIntegers, pkt) + assert [x.val for x in decoded.values] == [1, 2, 3] + + +def check_oer_field_choice(): + # type: () -> None + as_int = OERChoiceField(c=ASN1_INTEGER(99)) + assert raw(as_int) == b"\x02\x01c" + decoded = _roundtrip(OERChoiceField, as_int) + assert decoded.c.val == 99 + + as_str = OERChoiceField(c=ASN1_STRING("x")) + assert raw(as_str) == b"\x04\x01x" + decoded = _roundtrip(OERChoiceField, as_str) + assert decoded.c.val == b"x" + + +def check_oer_packet_record(): + # type: () -> None + pkt = OERRecord( + id=42, flag=True, label="hi", extra=7, values=[1, 2, 3], + ) + expected = ( + b"\x01*\xff\x02hi\xa0\x01\x07" + b"\x01\x03\x01\x01\x01\x02\x01\x03" + ) + assert raw(pkt) == expected + decoded = _roundtrip(OERRecord, pkt) + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"hi" + assert decoded.extra.val == 7 + assert [x.val for x in decoded.values] == [1, 2, 3] + + empty = OERRecord(id=1, flag=False, label="", extra=None, values=[]) + assert raw(empty) == b"\x01\x01\x00\x00\x01\x00" + decoded = _roundtrip(OERRecord, empty) + assert decoded.id.val == 1 + assert decoded.flag.val == 0 + assert decoded.label.val == b"" + assert decoded.extra is None + assert [x.val for x in decoded.values] == [] From 82ef84805baf66add2ca20fca5725e70ea45dd9e Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 1 Jul 2026 20:49:30 +0200 Subject: [PATCH 02/15] Added ASN.1 UPER encoding AI-Assisted: yes (Cursor AI) --- .config/codespell_ignore.txt | 5 + scapy/all.py | 1 + scapy/asn1/uper.py | 1297 +++++++++++++++++++++++++ scapy/asn1fields.py | 316 +++++- test/scapy/layers/asn1.uts | 156 +++ test/scapy/layers/uper_asn1scc_iop.py | 261 +++++ test/scapy/layers/uper_codec.py | 186 ++++ test/scapy/layers/uper_fuzz.py | 133 +++ test/scapy/layers/uper_helpers.py | 122 +++ test/scapy/layers/uper_iop.py | 251 +++++ test/scapy/layers/uper_packets.py | 542 +++++++++++ tox.ini | 1 + 12 files changed, 3253 insertions(+), 18 deletions(-) create mode 100644 scapy/asn1/uper.py create mode 100644 test/scapy/layers/uper_asn1scc_iop.py create mode 100644 test/scapy/layers/uper_codec.py create mode 100644 test/scapy/layers/uper_fuzz.py create mode 100644 test/scapy/layers/uper_helpers.py create mode 100644 test/scapy/layers/uper_iop.py create mode 100644 test/scapy/layers/uper_packets.py diff --git a/.config/codespell_ignore.txt b/.config/codespell_ignore.txt index 510f3e0c090..59e3f34dcd1 100644 --- a/.config/codespell_ignore.txt +++ b/.config/codespell_ignore.txt @@ -53,3 +53,8 @@ wan wanna webp widgits +uper +UPER +uPER +acn +ACN diff --git a/scapy/all.py b/scapy/all.py index 65752e8f5e5..286c7ac1ac6 100644 --- a/scapy/all.py +++ b/scapy/all.py @@ -42,6 +42,7 @@ from scapy.asn1.asn1 import * from scapy.asn1.ber import * from scapy.asn1.oer import * +from scapy.asn1.uper import * from scapy.asn1.mib import * from scapy.pipetool import * diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py new file mode 100644 index 00000000000..cfdf2e97e4b --- /dev/null +++ b/scapy/asn1/uper.py @@ -0,0 +1,1297 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +Unaligned Packed Encoding Rules (UPER) for ASN.1 + +As specified in ITU-T X.691 | ISO/IEC 8825-2. + +UPER is registered on ``ASN1_Codecs.PER``. Schema-driven encoding and decoding +(``ASN1F_SEQUENCE``, ``ASN1F_CHOICE``, ``ASN1F_SEQUENCE_OF``, +``ASN1F_ENUMERATED``) is supported for common field types. Not supported yet: +explicit/implicit tagging, SET, extension markers, +``ASN1F_CHOICE``/``ASN1F_SEQUENCE_OF`` with nested ``ASN1_Packet`` +alternatives, REAL, and PER-visible character string permuted alphabets. +""" + +import binascii + +from scapy.error import warning +from scapy.compat import orb, bytes_encode +from scapy.utils import binrepr, inet_aton, inet_ntoa +from scapy.asn1.ber import BER_num_dec, BER_num_enc +from scapy.asn1.asn1 import ( + ASN1_BADTAG, + ASN1_BadTag_Decoding_Error, + ASN1_Class, + ASN1_Class_UNIVERSAL, + ASN1_Codecs, + ASN1_DECODING_ERROR, + ASN1_Decoding_Error, + ASN1_Encoding_Error, + ASN1_Error, + ASN1_Object, + _ASN1_ERROR, +) + +from typing import ( + Any, + AnyStr, + Dict, + Generic, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, + cast, +) + + +################### +# UPER encoding # +################### + + +class UPER_Encoding_Error(ASN1_Encoding_Error): + def __init__(self, + msg, # type: str + encoded=None, # type: Optional[Union['UPERcodec_Object[Any]', str]] + remaining=b"" # type: bytes + ): + # type: (...) -> None + Exception.__init__(self, msg) + self.remaining = remaining + self.encoded = encoded + + def __str__(self): + # type: () -> str + s = Exception.__str__(self) + if isinstance(self.encoded, ASN1_Object): + s += "\n### Already encoded ###\n%s" % self.encoded.strshow() + else: + s += "\n### Already encoded ###\n%r" % self.encoded + s += "\n### Remaining ###\n%r" % self.remaining + return s + + +class UPER_Decoding_Error(ASN1_Decoding_Error): + def __init__(self, + msg, # type: str + decoded=None, # type: Optional[Any] + remaining=b"" # type: bytes + ): + # type: (...) -> None + Exception.__init__(self, msg) + self.remaining = remaining + self.decoded = decoded + + def __str__(self): + # type: () -> str + s = Exception.__str__(self) + if isinstance(self.decoded, ASN1_Object): + s += "\n### Already decoded ###\n%s" % self.decoded.strshow() + else: + s += "\n### Already decoded ###\n%r" % self.decoded + s += "\n### Remaining ###\n%r" % self.remaining + return s + + +class UPER_BadTag_Decoding_Error(UPER_Decoding_Error, + ASN1_BadTag_Decoding_Error): + pass + + +def UPER_bits_for_range(size): + # type: (int) -> int + if size <= 0: + return 0 + return size.bit_length() + + +class UPER_Encoder(object): + def __init__(self): + # type: () -> None + self.number_of_bits = 0 + self.value = 0 + self.chunks_number_of_bits = 0 + self.chunks = [] # type: List[List[int]] + + def append_bit(self, bit): + # type: (int) -> None + self.number_of_bits += 1 + self.value <<= 1 + self.value |= 1 if bit else 0 + + def append_bits(self, data, number_of_bits): + # type: (bytes, int) -> None + if number_of_bits == 0: + return + value = int(binascii.hexlify(data), 16) + value >>= (8 * len(data) - number_of_bits) + self.append_non_negative_binary_integer(value, number_of_bits) + + def append_non_negative_binary_integer(self, value, number_of_bits): + # type: (int, int) -> None + if number_of_bits == 0: + return + if self.number_of_bits > 4096: + self.chunks.append([self.value, self.number_of_bits]) + self.chunks_number_of_bits += self.number_of_bits + self.number_of_bits = 0 + self.value = 0 + self.number_of_bits += number_of_bits + self.value <<= number_of_bits + self.value |= value & ((1 << number_of_bits) - 1) + + def append_bytes(self, data): + # type: (bytes) -> None + self.append_bits(data, 8 * len(data)) + + def append_length_determinant(self, length): + # type: (int) -> int + if length < 128: + encoded = bytes([length]) + elif length < 16384: + encoded = bytes([(0x80 | (length >> 8)), (length & 0xff)]) + elif length < 32768: + encoded = b"\xc1" + length = 16384 + elif length < 49152: + encoded = b"\xc2" + length = 32768 + elif length < 65536: + encoded = b"\xc3" + length = 49152 + else: + encoded = b"\xc4" + length = 65536 + self.append_bytes(encoded) + return length + + def append_unconstrained_whole_number(self, value): + # type: (int) -> None + number_of_bits = 0 if value == 0 else value.bit_length() + if value < 0: + number_of_bytes = (number_of_bits + 7) // 8 + enc = (1 << (8 * number_of_bytes)) + value + if enc & (1 << (8 * number_of_bytes - 1)) == 0: + enc |= (0xff << (8 * number_of_bytes)) + number_of_bytes += 1 + elif value > 0: + number_of_bytes = (number_of_bits + 7) // 8 + if number_of_bits == 8 * number_of_bytes: + number_of_bytes += 1 + enc = value + else: + number_of_bytes = 1 + enc = 0 + self.append_length_determinant(number_of_bytes) + self.append_non_negative_binary_integer(enc, 8 * number_of_bytes) + + def as_bytes(self): + # type: () -> bytes + value = 0 + number_of_bits = 0 + for chunk_value, chunk_number_of_bits in self.chunks: + value <<= chunk_number_of_bits + value |= chunk_value + number_of_bits += chunk_number_of_bits + value <<= self.number_of_bits + value |= self.value + number_of_bits += self.number_of_bits + if number_of_bits == 0: + return b"" + number_of_alignment_bits = (8 - (number_of_bits % 8)) % 8 + value <<= number_of_alignment_bits + number_of_bits += number_of_alignment_bits + value |= (0x80 << number_of_bits) + hexval = hex(value)[4:].rstrip("L") + if len(hexval) % 2: + hexval = "0" + hexval + return binascii.unhexlify(hexval) + + +def _uper_significant_bit_count(data): + # type: (bytes) -> int + if not data: + return 0 + total = 8 * len(data) + dec = UPER_Decoder(data) + start = dec._read_offset() + bits = dec.value[start:start + total] + end = len(bits) + while end > 0 and bits[end - 1] == "0": + end -= 1 + trimmed = total - end + if trimmed > 0 and trimmed <= 8: + return end + return total + + +def UPER_append_encoded(enc, data): + # type: (UPER_Encoder, bytes) -> None + if not data: + return + dec = UPER_Decoder(data) + start = dec._read_offset() + nbits = _uper_significant_bit_count(data) + for i in range(nbits): + enc.append_bit(int(dec.value[start + i])) + + +def UPER_join_encodings(*parts): + # type: (*bytes) -> bytes + enc = UPER_Encoder() + for part in parts: + UPER_append_encoded(enc, part) + return enc.as_bytes() + + +def UPER_optional_presence_enc(bits, enc=None): + # type: (List[int], Optional[UPER_Encoder]) -> bytes + standalone = enc is None + if enc is None: + enc = UPER_Encoder() + for bit in bits: + enc.append_bit(bit) + return enc.as_bytes() if standalone else b"" + + +def UPER_count_enc(count, enc=None): + # type: (int, Optional[UPER_Encoder]) -> bytes + standalone = enc is None + if enc is None: + enc = UPER_Encoder() + enc.append_length_determinant(count) + return enc.as_bytes() if standalone else b"" + + +def UPER_has_unexpected_remainder(dec): + # type: (UPER_Decoder) -> bool + if dec.number_of_bits == 0: + return False + offset = dec._read_offset() + return dec.value[offset:].strip("0") != "" + + +def UPER_count_dec(s, dec=None): + # type: (bytes, Optional[UPER_Decoder]) -> Tuple[int, bytes] + standalone = dec is None + if dec is None: + dec = UPER_Decoder(s) + count = dec.read_length_determinant() + if standalone: + return count, dec.remaining() + return count, b"" + + +class UPER_Decoder(object): + def __init__(self, encoded): + # type: (bytes) -> None + self.number_of_bits = 8 * len(encoded) + self.total_number_of_bits = self.number_of_bits + if encoded: + value = int(binascii.hexlify(encoded), 16) + value |= (0x80 << self.number_of_bits) + self.value = bin(value)[10:] + else: + self.value = "" + + def _read_offset(self): + # type: () -> int + return self.total_number_of_bits - self.number_of_bits + + def read_bit(self): + # type: () -> int + if self.number_of_bits == 0: + raise UPER_Decoding_Error("UPER_Decoder: out of data") + offset = self._read_offset() + bit = int(self.value[offset]) + self.number_of_bits -= 1 + return bit + + def read_bits(self, number_of_bits): + # type: (int) -> bytes + if number_of_bits > self.number_of_bits: + raise UPER_Decoding_Error("UPER_Decoder: out of data") + if number_of_bits == 0: + return b"" + offset = self._read_offset() + bits = self.value[offset:offset + number_of_bits] + self.number_of_bits -= number_of_bits + value = "10000000" + bits + number_of_alignment_bits = (8 - (number_of_bits % 8)) + if number_of_alignment_bits != 8: + value += "0" * number_of_alignment_bits + hexval = hex(int(value, 2))[4:].rstrip("L") + if len(hexval) % 2: + hexval = "0" + hexval + return binascii.unhexlify(hexval) + + def remaining(self): + # type: () -> bytes + if self.number_of_bits == 0: + return b"" + offset = self._read_offset() + bits = self.value[offset:offset + self.number_of_bits] + value = "10000000" + bits + number_of_alignment_bits = (8 - (self.number_of_bits % 8)) + if number_of_alignment_bits != 8: + value += "0" * number_of_alignment_bits + hexval = hex(int(value, 2))[4:].rstrip("L") + if len(hexval) % 2: + hexval = "0" + hexval + return binascii.unhexlify(hexval) + + def read_bytes(self, number_of_bytes): + # type: (int) -> bytes + return self.read_bits(8 * number_of_bytes) + + def read_non_negative_binary_integer(self, number_of_bits): + # type: (int) -> int + if number_of_bits > self.number_of_bits: + raise UPER_Decoding_Error("UPER_Decoder: out of data") + if number_of_bits == 0: + return 0 + offset = self._read_offset() + bits = self.value[offset:offset + number_of_bits] + self.number_of_bits -= number_of_bits + return int(bits, 2) + + def read_length_determinant(self): + # type: () -> int + value = self.read_non_negative_binary_integer(8) + if (value & 0x80) == 0x00: + return value + if (value & 0xc0) == 0x80: + return ((value & 0x7f) << 8) | self.read_non_negative_binary_integer(8) + mapping = {0xc1: 16384, 0xc2: 32768, 0xc3: 49152, 0xc4: 65536} + if value in mapping: + return mapping[value] + raise UPER_Decoding_Error( + "UPER_Decoder: bad length determinant 0x%02x" % value + ) + + def read_unconstrained_whole_number(self): + # type: () -> int + number_of_bytes = self.read_length_determinant() + enc = self.read_non_negative_binary_integer(8 * number_of_bytes) + sign_bit = 1 << (8 * number_of_bytes - 1) + if enc & sign_bit: + return enc - (1 << (8 * number_of_bytes)) + return enc + + def consume_input(self): + # type: () -> None + self.number_of_bits = 0 + + +def UPER_constrained_int_enc(value, minimum, maximum, enc=None): + # type: (int, int, int, Optional[UPER_Encoder]) -> bytes + standalone = enc is None + if enc is None: + enc = UPER_Encoder() + size = maximum - minimum + enc.append_non_negative_binary_integer( + value - minimum, UPER_bits_for_range(size) + ) + return enc.as_bytes() if standalone else b"" + + +def UPER_constrained_int_dec(s, minimum, maximum): + # type: (bytes, int, int) -> Tuple[int, bytes] + dec = UPER_Decoder(s) + size = maximum - minimum + value = dec.read_non_negative_binary_integer(UPER_bits_for_range(size)) + dec.consume_input() + return value + minimum, b"" + + +def UPER_unconstrained_int_enc(value, enc=None): + # type: (int, Optional[UPER_Encoder]) -> bytes + standalone = enc is None + if enc is None: + enc = UPER_Encoder() + enc.append_unconstrained_whole_number(value) + return enc.as_bytes() if standalone else b"" + + +def UPER_unconstrained_int_dec(s): + # type: (bytes) -> Tuple[int, bytes] + dec = UPER_Decoder(s) + value = dec.read_unconstrained_whole_number() + remain = dec.remaining() + return value, remain + + +def UPER_boolean_enc(value, enc=None): + # type: (int, Optional[UPER_Encoder]) -> bytes + standalone = enc is None + if enc is None: + enc = UPER_Encoder() + enc.append_bit(1 if value else 0) + return enc.as_bytes() if standalone else b"" + + +def UPER_boolean_dec(s): + # type: (bytes) -> Tuple[int, bytes] + dec = UPER_Decoder(s) + value = dec.read_bit() + dec.consume_input() + return value, b"" + + +def UPER_octet_string_enc(data, minimum=None, maximum=None, enc=None): + # type: (bytes, Optional[int], Optional[int], Optional[UPER_Encoder]) -> bytes + standalone = enc is None + if enc is None: + enc = UPER_Encoder() + if minimum is not None and maximum is not None and minimum == maximum: + enc.append_bytes(data) + elif minimum is not None and maximum is not None: + enc.append_non_negative_binary_integer( + len(data) - minimum, + UPER_bits_for_range(maximum - minimum), + ) + enc.append_bytes(data) + else: + enc.append_length_determinant(len(data)) + enc.append_bytes(data) + return enc.as_bytes() if standalone else b"" + + +def UPER_octet_string_dec(s, minimum=None, maximum=None, dec=None): + # type: (bytes, Optional[int], Optional[int], Optional[UPER_Decoder]) -> Tuple[bytes, bytes] # noqa: E501 + standalone = dec is None + if dec is None: + dec = UPER_Decoder(s) + if minimum is not None and maximum is not None and minimum == maximum: + length = minimum + elif minimum is not None and maximum is not None: + length = minimum + dec.read_non_negative_binary_integer( + UPER_bits_for_range(maximum - minimum) + ) + else: + length = dec.read_length_determinant() + data = dec.read_bytes(length) + if standalone: + return data, dec.remaining() + return data, b"" + + +def UPER_choice_index_enc(index, number_of_choices, enc=None): + # type: (int, int, Optional[UPER_Encoder]) -> bytes + standalone = enc is None + if enc is None: + enc = UPER_Encoder() + enc.append_non_negative_binary_integer( + index, UPER_bits_for_range(number_of_choices - 1) + ) + return enc.as_bytes() if standalone else b"" + + +def UPER_choice_index_dec(s, number_of_choices, dec=None): + # type: (bytes, int, Optional[UPER_Decoder]) -> Tuple[int, bytes] + standalone = dec is None + if dec is None: + dec = UPER_Decoder(s) + index = dec.read_non_negative_binary_integer( + UPER_bits_for_range(number_of_choices - 1) + ) + if standalone: + return index, dec.remaining() + return index, b"" + + +class UPERcodec_metaclass(type): + def __new__(cls, + name, # type: str + bases, # type: Tuple[type, ...] + dct # type: Dict[str, Any] + ): + # type: (...) -> Type['UPERcodec_Object[Any]'] + c = cast('Type[UPERcodec_Object[Any]]', + super(UPERcodec_metaclass, cls).__new__(cls, name, bases, dct)) + try: + c.tag.register(c.codec, c) + except Exception: + warning("Error registering %r for %r" % (c.tag, c.codec)) + return c + + +_K = TypeVar('_K') + + +class UPERcodec_Object(Generic[_K], metaclass=UPERcodec_metaclass): + codec = ASN1_Codecs.PER + tag = ASN1_Class_UNIVERSAL.ANY + + @classmethod + def asn1_object(cls, val): + # type: (_K) -> ASN1_Object[_K] + return cls.tag.asn1_object(val) + + @classmethod + def check_string(cls, s): + # type: (bytes) -> None + if not s and cls.tag != ASN1_Class_UNIVERSAL.NULL: + raise UPER_Decoding_Error( + "%s: Got empty object while expecting %r" % + (cls.__name__, cls.tag), remaining=s + ) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + uper_enum_values=None, # type: Optional[List[int]] + ): + # type: (...) -> Tuple[ASN1_Object[Any], bytes] + raise UPER_Decoding_Error( + "%s: Cannot decode unknown UPER type without context" % + cls.__name__, remaining=s + ) + + @classmethod + def dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + uper_enum_values=None, # type: Optional[List[int]] + ): + # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] + dec_kwargs = {} # type: Dict[str, Any] + if uper_enum_values is not None: + dec_kwargs["uper_enum_values"] = uper_enum_values + if not safe: + return cls.do_dec( + s, context, safe, size_len, uper_min, uper_max, oer_unsigned, + **dec_kwargs + ) + try: + return cls.do_dec( + s, context, safe, size_len, uper_min, uper_max, oer_unsigned, + **dec_kwargs + ) + except UPER_BadTag_Decoding_Error as e: + o, remain = UPERcodec_Object.dec( + e.remaining, context, safe, size_len, uper_min, uper_max, + oer_unsigned, uper_enum_values=uper_enum_values, + ) + return ASN1_BADTAG(o), remain + except UPER_Decoding_Error as e: + return ASN1_DECODING_ERROR(s, exc=e), b"" + except ASN1_Error as e: + return ASN1_DECODING_ERROR(s, exc=e), b"" + + @classmethod + def safedec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + uper_enum_values=None, # type: Optional[List[int]] + ): + # type: (...) -> Tuple[Union[_ASN1_ERROR, ASN1_Object[_K]], bytes] + return cls.dec( + s, context, safe=True, + size_len=size_len, uper_min=uper_min, uper_max=uper_max, + oer_unsigned=oer_unsigned, uper_enum_values=uper_enum_values, + ) + + @classmethod + def enc(cls, s, size_len=0, uper_min=None, uper_max=None): + # type: (_K, Optional[int], Optional[int], Optional[int]) -> bytes + if isinstance(s, (str, bytes)): + return UPERcodec_STRING.enc(s, size_len=size_len, + uper_min=uper_min, uper_max=uper_max) + else: + try: + return UPERcodec_INTEGER.enc( + int(s), + size_len=size_len, + uper_min=uper_min, + uper_max=uper_max, + ) + except Exception: + raise UPER_Encoding_Error( + "Cannot encode value %r for %s" % (s, cls.__name__), + encoded=s + ) + + +ASN1_Codecs.PER.register_stem(UPERcodec_Object) + + +######################### +# UPERcodec objects # +######################### + + +def _uper_int_range(size_len, uper_min, uper_max, oer_unsigned=False): + # type: (Optional[int], Optional[int], Optional[int], bool) -> Tuple[Optional[int], Optional[int]] # noqa: E501 + if uper_min is not None or uper_max is not None: + return uper_min, uper_max + if size_len in (1, 2, 4, 8) and oer_unsigned: + return 0, (256 ** size_len) - 1 + return None, None + + +class UPERcodec_INTEGER(UPERcodec_Object[int]): + tag = ASN1_Class_UNIVERSAL.INTEGER + + @classmethod + def encode_into(cls, + enc, # type: UPER_Encoder + i, # type: int + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> None + minimum, maximum = _uper_int_range(size_len, uper_min, uper_max, oer_unsigned) + if minimum is not None and maximum is not None: + UPER_constrained_int_enc(i, minimum, maximum, enc=enc) + else: + UPER_unconstrained_int_enc(i, enc=enc) + + @classmethod + def dec_from_decoder(cls, + dec, # type: UPER_Decoder + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> ASN1_Object[int] + minimum, maximum = _uper_int_range(size_len, uper_min, uper_max, oer_unsigned) + if minimum is not None and maximum is not None: + value = dec.read_non_negative_binary_integer( + UPER_bits_for_range(maximum - minimum) + ) + minimum + else: + value = dec.read_unconstrained_whole_number() + return cls.asn1_object(value) + + @classmethod + def enc(cls, i, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False): + # type: (int, Optional[int], Optional[int], Optional[int], bool) -> bytes + enc = UPER_Encoder() + cls.encode_into(enc, i, size_len, uper_min, uper_max, oer_unsigned) + return enc.as_bytes() + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[int], bytes] + minimum, maximum = _uper_int_range(size_len, uper_min, uper_max, oer_unsigned) + if minimum is not None and maximum is not None: + x, t = UPER_constrained_int_dec(s, minimum, maximum) + else: + x, t = UPER_unconstrained_int_dec(s) + return cls.asn1_object(x), t + + +class UPERcodec_BOOLEAN(UPERcodec_Object[int]): + tag = ASN1_Class_UNIVERSAL.BOOLEAN + + @classmethod + def encode_into(cls, + enc, # type: UPER_Encoder + i, # type: int + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> None + UPER_boolean_enc(i, enc=enc) + + @classmethod + def dec_from_decoder(cls, + dec, # type: UPER_Decoder + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> ASN1_Object[int] + return cls.asn1_object(dec.read_bit()) + + @classmethod + def enc(cls, i, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False): + # type: (int, Optional[int], Optional[int], Optional[int], bool) -> bytes + enc = UPER_Encoder() + cls.encode_into(enc, i, size_len, uper_min, uper_max, oer_unsigned) + return enc.as_bytes() + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[int], bytes] + x, t = UPER_boolean_dec(s) + return cls.asn1_object(x), t + + +def _uper_bytes_to_bitstr(data, nbits): + # type: (bytes, int) -> str + bitstr = "".join(binrepr(orb(x)).zfill(8) for x in data) + return bitstr[:nbits] + + +def _uper_bit_string_parts(_s): + # type: (Any) -> Tuple[bytes, int] + if isinstance(_s, tuple) and len(_s) == 2: + data, nbits = _s + return bytes_encode(data), nbits + if isinstance(_s, str) and _s and all(c in "01" for c in _s): + nbits = len(_s) + padded = _s + "0" * ((8 - nbits % 8) % 8) + data = int(padded or "0", 2).to_bytes( + max(1, len(padded) // 8), "big" + ) + return data, nbits + s = bytes_encode(_s) + return s, 8 * len(s) + + +class UPERcodec_BIT_STRING(UPERcodec_Object[str]): + tag = ASN1_Class_UNIVERSAL.BIT_STRING + + @classmethod + def encode_into(cls, + enc, # type: UPER_Encoder + _s, # type: Any + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> None + s, nbits = _uper_bit_string_parts(_s) + minimum = uper_min + maximum = uper_max + if size_len: + minimum = maximum = size_len + if minimum is not None and maximum is not None and minimum == maximum: + enc.append_bits(s, nbits) + elif minimum is not None and maximum is not None: + enc.append_non_negative_binary_integer( + nbits - minimum, UPER_bits_for_range(maximum - minimum) + ) + enc.append_bits(s, nbits) + else: + enc.append_length_determinant((nbits + 7) // 8) + enc.append_bytes(s) + + @classmethod + def dec_from_decoder(cls, + dec, # type: UPER_Decoder + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> ASN1_Object[str] + minimum = uper_min + maximum = uper_max + if size_len: + minimum = maximum = size_len + if minimum is not None and maximum is not None and minimum == maximum: + nbits = minimum + elif minimum is not None and maximum is not None: + nbits = minimum + dec.read_non_negative_binary_integer( + UPER_bits_for_range(maximum - minimum) + ) + else: + nbytes = dec.read_length_determinant() + raw = dec.read_bytes(nbytes) + nbits = 8 * nbytes + return cls.asn1_object(_uper_bytes_to_bitstr(raw, nbits)) + raw = dec.read_bits(nbits) + return cls.asn1_object(_uper_bytes_to_bitstr(raw, nbits)) + + @classmethod + def enc(cls, _s, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False): + # type: (Any, Optional[int], Optional[int], Optional[int], bool) -> bytes + enc = UPER_Encoder() + cls.encode_into(enc, _s, size_len, uper_min, uper_max, oer_unsigned) + return enc.as_bytes() + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[str], bytes] + dec = UPER_Decoder(s) + minimum = uper_min + maximum = uper_max + if minimum is not None and maximum is not None and minimum == maximum: + nbits = minimum + elif minimum is not None and maximum is not None: + nbits = minimum + dec.read_non_negative_binary_integer( + UPER_bits_for_range(maximum - minimum) + ) + else: + nbytes = dec.read_length_determinant() + raw = dec.read_bytes(nbytes) + nbits = 8 * nbytes + return cls.asn1_object(_uper_bytes_to_bitstr(raw, nbits)), dec.remaining() + raw = dec.read_bits(nbits) + return cls.asn1_object(_uper_bytes_to_bitstr(raw, nbits)), dec.remaining() + + +def _uper_octet_string_bounds(size_len, uper_min, uper_max): + # type: (Optional[int], Optional[int], Optional[int]) -> Tuple[Optional[int], Optional[int]] # noqa: E501 + if size_len: + return size_len, size_len + return uper_min, uper_max + + +class UPERcodec_STRING(UPERcodec_Object[str]): + tag = ASN1_Class_UNIVERSAL.STRING + + @classmethod + def encode_into(cls, + enc, # type: UPER_Encoder + _s, # type: Union[str, bytes] + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> None + s = bytes_encode(_s) + minimum, maximum = _uper_octet_string_bounds( + size_len, uper_min, uper_max, + ) + UPER_octet_string_enc(s, minimum, maximum, enc=enc) + + @classmethod + def dec_from_decoder(cls, + dec, # type: UPER_Decoder + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> ASN1_Object[Any] + minimum, maximum = _uper_octet_string_bounds( + size_len, uper_min, uper_max, + ) + raw, _ = UPER_octet_string_dec(b"", minimum, maximum, dec=dec) + return cls.asn1_object(raw) + + @classmethod + def enc(cls, _s, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False): + # type: (Union[str, bytes], Optional[int], Optional[int], Optional[int], bool) -> bytes # noqa: E501 + enc = UPER_Encoder() + cls.encode_into(enc, _s, size_len, uper_min, uper_max, oer_unsigned) + return enc.as_bytes() + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[Any], bytes] + minimum, maximum = _uper_octet_string_bounds( + size_len, uper_min, uper_max, + ) + raw, remain = UPER_octet_string_dec(s, minimum, maximum) + return cls.asn1_object(raw), remain + + +class UPERcodec_NULL(UPERcodec_Object[None]): + tag = ASN1_Class_UNIVERSAL.NULL + + @classmethod + def encode_into(cls, + enc, # type: UPER_Encoder + _s, # type: Any + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> None + return + + @classmethod + def dec_from_decoder(cls, + dec, # type: UPER_Decoder + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> ASN1_Object[None] + return cls.asn1_object(None) + + @classmethod + def enc(cls, _s, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False): + # type: (Any, Optional[int], Optional[int], Optional[int], bool) -> bytes + return b"" + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[None], bytes] + return cls.asn1_object(None), s + + +class UPERcodec_OID(UPERcodec_Object[bytes]): + tag = ASN1_Class_UNIVERSAL.OID + + @classmethod + def enc(cls, _oid, size_len=0, uper_min=None, uper_max=None): + # type: (AnyStr, Optional[int], Optional[int], Optional[int]) -> bytes + oid = bytes_encode(_oid) + if oid: + lst = [int(x) for x in oid.split(b".")] + lst = [40 * lst[0] + lst[1]] + lst[2:] + else: + lst = [] + body = b"".join(BER_num_enc(k) for k in lst) + enc = UPER_Encoder() + enc.append_length_determinant(len(body)) + enc.append_bytes(body) + return enc.as_bytes() + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[bytes], bytes] + dec = UPER_Decoder(s) + l = dec.read_length_determinant() + content = dec.read_bytes(l) + lst = [] + while content: + val, content = BER_num_dec(content) + lst.append(val) + if len(lst) > 0: + lst.insert(0, lst[0] // 40) + lst[1] %= 40 + return ( + cls.asn1_object(b".".join(str(k).encode('ascii') for k in lst)), + dec.remaining(), + ) + + +def UPER_enumerated_enc(value, + enum_values, # type: List[int] + enc=None # type: Optional[UPER_Encoder] + ): + # type: (int, List[int], Optional[UPER_Encoder]) -> bytes + standalone = enc is None + if enc is None: + enc = UPER_Encoder() + if not enum_values: + raise UPER_Encoding_Error("UPER_enumerated_enc: empty enumeration") + try: + index = enum_values.index(value) + except ValueError: + raise UPER_Encoding_Error( + "UPER_enumerated_enc: unknown enumeration value %r" % value + ) + UPER_choice_index_enc(index, len(enum_values), enc=enc) + return enc.as_bytes() if standalone else b"" + + +def UPER_enumerated_dec(s, + enum_values, # type: List[int] + dec=None # type: Optional[UPER_Decoder] + ): + # type: (bytes, List[int], Optional[UPER_Decoder]) -> Tuple[int, bytes] + standalone = dec is None + if dec is None: + dec = UPER_Decoder(s) + if not enum_values: + raise UPER_Decoding_Error("UPER_enumerated_dec: empty enumeration") + index, _ = UPER_choice_index_dec(b"", len(enum_values), dec=dec) + if index >= len(enum_values): + raise UPER_Decoding_Error( + "UPER_enumerated_dec: index %i out of range" % index + ) + if standalone: + dec.consume_input() + return enum_values[index], b"" + return enum_values[index], b"" + + +class UPERcodec_ENUMERATED(UPERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.ENUMERATED + + @classmethod + def encode_into(cls, + enc, # type: UPER_Encoder + i, # type: int + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + uper_enum_values=None, # type: Optional[List[int]] + ): + # type: (...) -> None + if uper_enum_values is not None: + UPER_enumerated_enc(i, uper_enum_values, enc=enc) + return + minimum = uper_min if uper_min is not None else 0 + maximum = uper_max if uper_max is not None else size_len + if maximum is None: + maximum = max(i, 0) + UPER_constrained_int_enc(i, minimum, maximum, enc=enc) + + @classmethod + def dec_from_decoder(cls, + dec, # type: UPER_Decoder + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + uper_enum_values=None, # type: Optional[List[int]] + ): + # type: (...) -> ASN1_Object[int] + if uper_enum_values is not None: + value, _ = UPER_enumerated_dec(b"", uper_enum_values, dec=dec) + return cls.asn1_object(value) + minimum = uper_min if uper_min is not None else 0 + maximum = uper_max if uper_max is not None else size_len + if maximum is None: + raise UPER_Decoding_Error("UPERcodec_ENUMERATED: missing range") + value = dec.read_non_negative_binary_integer( + UPER_bits_for_range(maximum - minimum) + ) + minimum + return cls.asn1_object(value) + + @classmethod + def enc(cls, + i, + size_len=0, + uper_min=None, + uper_max=None, + oer_unsigned=False, + uper_enum_values=None, + ): + # type: (int, Optional[int], Optional[int], Optional[int], bool, Optional[List[int]]) -> bytes # noqa: E501 + enc = UPER_Encoder() + cls.encode_into( + enc, i, size_len, uper_min, uper_max, oer_unsigned, + uper_enum_values=uper_enum_values, + ) + return enc.as_bytes() + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + uper_enum_values=None, # type: Optional[List[int]] + ): + # type: (...) -> Tuple[ASN1_Object[int], bytes] + if uper_enum_values is not None: + x, t = UPER_enumerated_dec(s, uper_enum_values) + return cls.asn1_object(x), t + minimum = uper_min if uper_min is not None else 0 + maximum = uper_max if uper_max is not None else size_len + if maximum is None: + raise UPER_Decoding_Error("UPERcodec_ENUMERATED: missing range") + x, t = UPER_constrained_int_dec(s, minimum, maximum) + return cls.asn1_object(x), t + + +class UPERcodec_SEQUENCE(UPERcodec_Object[Union[bytes, List[Any]]]): + tag = ASN1_Class_UNIVERSAL.SEQUENCE + + @classmethod + def encode_into(cls, + enc, # type: UPER_Encoder + _ll, # type: Union[bytes, List[UPERcodec_Object[Any]]] + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> None + if isinstance(_ll, bytes): + UPER_append_encoded(enc, _ll) + + @classmethod + def enc(cls, _ll, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False): + # type: (Union[bytes, List[UPERcodec_Object[Any]]], Optional[int], Optional[int], Optional[int], bool) -> bytes # noqa: E501 + if isinstance(_ll, bytes): + return _ll + raise UPER_Encoding_Error( + "UPERcodec_SEQUENCE: schema-defined field order required" + ) + + @classmethod + def do_dec(cls, + s, # type: bytes + context=None, # type: Optional[Type[ASN1_Class]] + safe=False, # type: bool + size_len=0, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + oer_unsigned=False, # type: bool + ): + # type: (...) -> Tuple[ASN1_Object[Union[bytes, List[Any]]], bytes] + raise UPER_Decoding_Error( + "UPERcodec_SEQUENCE: decoding requires schema-defined field order", + remaining=s + ) + + +class UPERcodec_SET(UPERcodec_SEQUENCE): + tag = ASN1_Class_UNIVERSAL.SET + + +class UPERcodec_IPADDRESS(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.IPADDRESS + + @classmethod + def enc(cls, ipaddr_ascii, size_len=0, uper_min=None, uper_max=None): + # type: (str, Optional[int], Optional[int], Optional[int]) -> bytes + try: + s = inet_aton(ipaddr_ascii) + except Exception: + raise UPER_Encoding_Error("IPv4 address could not be encoded") + return UPER_octet_string_enc(s, 4, 4) + + @classmethod + def do_dec(cls, s, context=None, safe=False, + size_len=0, uper_min=None, uper_max=None, + oer_unsigned=False): + # type: (bytes, Optional[Any], bool, Optional[int], Optional[int], Optional[int], bool) -> Tuple[ASN1_Object[str], bytes] # noqa: E501 + raw, remain = UPER_octet_string_dec(s, 4, 4) + try: + ipaddr_ascii = inet_ntoa(raw) + except Exception: + raise UPER_Decoding_Error( + "IP address could not be decoded", + remaining=s, + ) + return cls.asn1_object(ipaddr_ascii), remain + + +class UPERcodec_COUNTER32(UPERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.COUNTER32 + + +class UPERcodec_COUNTER64(UPERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.COUNTER64 + + +class UPERcodec_GAUGE32(UPERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.GAUGE32 + + +class UPERcodec_TIME_TICKS(UPERcodec_INTEGER): + tag = ASN1_Class_UNIVERSAL.TIME_TICKS + + +# string aliases +class UPERcodec_UTF8_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.UTF8_STRING + + +class UPERcodec_NUMERIC_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.NUMERIC_STRING + + +class UPERcodec_PRINTABLE_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.PRINTABLE_STRING + + +class UPERcodec_T61_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.T61_STRING + + +class UPERcodec_VIDEOTEX_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.VIDEOTEX_STRING + + +class UPERcodec_IA5_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.IA5_STRING + + +class UPERcodec_GENERAL_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.GENERAL_STRING + + +class UPERcodec_UTC_TIME(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.UTC_TIME + + +class UPERcodec_GENERALIZED_TIME(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.GENERALIZED_TIME + + +class UPERcodec_ISO646_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.ISO646_STRING + + +class UPERcodec_UNIVERSAL_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.UNIVERSAL_STRING + + +class UPERcodec_BMP_STRING(UPERcodec_STRING): + tag = ASN1_Class_UNIVERSAL.BMP_STRING diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index a6dd27c9316..9a1b150010e 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -41,6 +41,15 @@ OER_unsigned_integer_dec, OER_unsigned_integer_enc, ) +from scapy.asn1.uper import ( + UPER_Decoding_Error, + UPER_Decoder, + UPER_Encoder, + UPER_choice_index_dec, + UPER_choice_index_enc, + UPER_count_dec, + UPER_has_unexpected_remainder, +) from scapy.base_classes import BasePacket from scapy.volatile import ( GeneralizedTime, @@ -105,6 +114,9 @@ def __init__(self, flexible_tag=False, # type: Optional[bool] size_len=None, # type: Optional[int] oer_unsigned=False, # type: Optional[bool] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + uper_enum_values=None, # type: Optional[List[int]] ): # type: (...) -> None if context is not None: @@ -118,6 +130,9 @@ def __init__(self, self.default = self.ASN1_tag.asn1_object(default) # type: ignore self.size_len = size_len self.oer_unsigned = oer_unsigned + self.uper_min = uper_min + self.uper_max = uper_max + self.uper_enum_values = uper_enum_values self.flexible_tag = flexible_tag if (implicit_tag is not None) and (explicit_tag is not None): err_msg = "field cannot be both implicitly and explicitly tagged" @@ -160,12 +175,14 @@ def m2i(self, pkt, s): explicit_tag=self.explicit_tag, safe=self.flexible_tag, _fname=self.name) - else: + elif pkt.ASN1_codec != ASN1_Codecs.PER: diff_tag, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag, safe=self.flexible_tag, _fname=self.name) + else: + diff_tag = None if diff_tag is not None: # this implies that flexible_tag was True if self.implicit_tag is not None: @@ -173,16 +190,34 @@ def m2i(self, pkt, s): elif self.explicit_tag is not None: self.explicit_tag = diff_tag codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - oer_kwargs = {} # type: Dict[str, Any] + codec_kwargs = {} # type: Dict[str, Any] if pkt.ASN1_codec == ASN1_Codecs.OER: - oer_kwargs = { + codec_kwargs = { "size_len": self.size_len or 0, "oer_unsigned": self.oer_unsigned, } + elif pkt.ASN1_codec == ASN1_Codecs.PER: + codec_kwargs = self._uper_codec_kwargs() if self.flexible_tag: - return codec.safedec(s, context=self.context, **oer_kwargs) # type: ignore + return codec.safedec( + s, context=self.context, **codec_kwargs + ) # type: ignore else: - return codec.dec(s, context=self.context, **oer_kwargs) # type: ignore + return codec.dec(s, context=self.context, **codec_kwargs) # type: ignore + + def m2i_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, UPER_Decoder) -> _A + codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + return cast( + _A, + codec.dec_from_decoder( # type: ignore[attr-defined] + dec, **self._uper_codec_kwargs(), + ), + ) + + def dissect_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, UPER_Decoder) -> None + self.set_val(pkt, self.m2i_from_decoder(pkt, dec)) def i2m(self, pkt, x): # type: (ASN1_Packet, Union[bytes, _I, _A]) -> bytes @@ -193,17 +228,28 @@ def i2m(self, pkt, x): x.tag == ASN1_Class_UNIVERSAL.RAW or x.tag == ASN1_Class_UNIVERSAL.ERROR or self.ASN1_tag == x.tag): - s = x.enc(pkt.ASN1_codec) + if pkt.ASN1_codec == ASN1_Codecs.PER: + codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + s = codec.enc(x.val, **self._uper_codec_kwargs()) + else: + s = x.enc(pkt.ASN1_codec) else: raise ASN1_Error("Encoding Error: got %r instead of an %r for field [%s]" % (x, self.ASN1_tag, self.name)) # noqa: E501 else: - s = self.ASN1_tag.get_codec(pkt.ASN1_codec).enc(x, size_len=self.size_len) + codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + size_len = self.size_len or 0 + if pkt.ASN1_codec == ASN1_Codecs.PER: + s = codec.enc(x, **self._uper_codec_kwargs(size_len)) + else: + s = codec.enc(x, size_len=size_len) if pkt.ASN1_codec == ASN1_Codecs.OER: return cast(bytes, OER_tagging_enc( s, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag, )) + if pkt.ASN1_codec == ASN1_Codecs.PER: + return s return BER_tagging_enc(s, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag) @@ -276,18 +322,61 @@ def copy(self): # type: () -> ASN1F_field[_I, _A] return copy.copy(self) + def _uper_codec_kwargs(self, size_len=None): + # type: (Optional[int]) -> Dict[str, Any] + kwargs = { + "size_len": (self.size_len if size_len is None else size_len) or 0, + "oer_unsigned": self.oer_unsigned, + "uper_min": self.uper_min, + "uper_max": self.uper_max, + } # type: Dict[str, Any] + if self.uper_enum_values is not None: + kwargs["uper_enum_values"] = self.uper_enum_values + return kwargs + def _encode_item(self, pkt, item): # type: (ASN1_Packet, Any) -> bytes if isinstance(item, ASN1_Object): + if pkt.ASN1_codec == ASN1_Codecs.PER: + codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + return codec.enc(item.val, **self._uper_codec_kwargs()) return item.enc(pkt.ASN1_codec) if hasattr(item, "self_build"): return cast("ASN1_Packet", item).self_build() codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) size_len = self.size_len or 0 if pkt.ASN1_codec == ASN1_Codecs.OER and self.oer_unsigned: - return codec.enc(item, size_len=size_len, oer_unsigned=True) # type: ignore[call-arg] + return codec.enc( + item, size_len=size_len, oer_unsigned=True + ) # type: ignore[call-arg] + if pkt.ASN1_codec == ASN1_Codecs.PER: + return codec.enc(item, **self._uper_codec_kwargs(size_len)) return codec.enc(item, size_len=size_len) + def _uper_encode_into(self, enc, pkt, value=None): + # type: (UPER_Encoder, ASN1_Packet, Any) -> None + if value is None: + value = getattr(pkt, self.name) + if value is None: + return + codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + if isinstance(value, ASN1_Object): + if (self.ASN1_tag == ASN1_Class_UNIVERSAL.ANY or + value.tag == ASN1_Class_UNIVERSAL.RAW or + value.tag == ASN1_Class_UNIVERSAL.ERROR or + self.ASN1_tag == value.tag): + raw = value.val + else: + raise ASN1_Error( + "Encoding Error: got %r instead of an %r for field [%s]" % + (value, self.ASN1_tag, self.name) + ) + else: + raw = value + codec.encode_into( # type: ignore[attr-defined] + enc, raw, **self._uper_codec_kwargs(), + ) + ############################ # Simple ASN1 Fields # @@ -335,6 +424,7 @@ def __init__(self, for k in keys: i2s[k] = enum[k] s2i[enum[k]] = k + self.uper_enum_values = list(keys) def i2m(self, pkt, # type: ASN1_Packet @@ -369,12 +459,16 @@ def __init__(self, context=None, # type: Optional[Any] implicit_tag=None, # type: Optional[int] explicit_tag=None, # type: Optional[int] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] ): # type: (...) -> None super(ASN1F_BIT_STRING, self).__init__( name, None, context=context, implicit_tag=implicit_tag, - explicit_tag=explicit_tag + explicit_tag=explicit_tag, + uper_min=uper_min, + uper_max=uper_max, ) if isinstance(default, (bytes, str)): self.default = ASN1_BIT_STRING(default, @@ -538,6 +632,13 @@ def m2i(self, pkt, s): if len(s) > 0: raise OER_Decoding_Error("unexpected remainder", remaining=s) return [], b"" + if pkt.ASN1_codec == ASN1_Codecs.PER: + dec = UPER_Decoder(s) + self._uper_dissect_from_decoder(pkt, dec) + if UPER_has_unexpected_remainder(dec): + raise UPER_Decoding_Error("unexpected remainder", + remaining=dec.remaining()) + return [], b"" diff_tag, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag, @@ -563,6 +664,27 @@ def m2i(self, pkt, s): raise BER_Decoding_Error("unexpected remainder", remaining=s) return [], remain + def _uper_dissect_from_decoder(self, pkt, dec): + # type: (Any, UPER_Decoder) -> None + optionals = [f for f in self.seq if isinstance(f, ASN1F_optional)] + presence = [dec.read_bit() for _ in optionals] + opt_idx = 0 + for obj in self.seq: + if isinstance(obj, ASN1F_optional): + if not presence[opt_idx]: + obj.set_val(pkt, None) + opt_idx += 1 + continue + opt_idx += 1 + try: + obj.dissect_from_decoder(pkt, dec) + except ASN1F_badsequence: + break + + def dissect_from_decoder(self, pkt, dec): + # type: (Any, UPER_Decoder) -> None + self._uper_dissect_from_decoder(pkt, dec) + def dissect(self, pkt, s): # type: (Any, bytes) -> bytes _, x = self.m2i(pkt, s) @@ -570,10 +692,24 @@ def dissect(self, pkt, s): def build(self, pkt): # type: (ASN1_Packet) -> bytes + if pkt.ASN1_codec == ASN1_Codecs.PER: + enc = UPER_Encoder() + self._uper_encode_into(enc, pkt) + return super(ASN1F_SEQUENCE, self).i2m(pkt, enc.as_bytes()) s = reduce(lambda x, y: x + y.build(pkt), self.seq, b"") return super(ASN1F_SEQUENCE, self).i2m(pkt, s) + def _uper_encode_into(self, enc, pkt, value=None): + # type: (UPER_Encoder, ASN1_Packet, Optional[Any]) -> None + optionals = [f for f in self.seq if isinstance(f, ASN1F_optional)] + for opt in optionals: + enc.append_bit(0 if opt.is_empty(pkt) else 1) + for obj in self.seq: + if isinstance(obj, ASN1F_optional) and obj.is_empty(pkt): + continue + obj._uper_encode_into(enc, pkt, value) + class ASN1F_SET(ASN1F_SEQUENCE): ASN1_tag = ASN1_Class_UNIVERSAL.SET @@ -631,6 +767,38 @@ def is_empty(self, # type: (...) -> bool return ASN1F_field.is_empty(self, pkt) + def _extract_packet_from_decoder(self, dec, pkt): + # type: (UPER_Decoder, ASN1_Packet) -> Tuple[Any, bytes] + if self.holds_packets: + raise UPER_Decoding_Error( + "UPER SEQUENCE OF packets is not supported yet" + ) + return self.fld.m2i_from_decoder(pkt, dec), b"" + + def m2i_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, UPER_Decoder) -> List[Any] + count, _ = UPER_count_dec(b"", dec=dec) + lst = [] + for _ in range(count): + item, _ = self._extract_packet_from_decoder(dec, pkt) + lst.append(item) + return lst + + def dissect_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, UPER_Decoder) -> None + self.set_val(pkt, self.m2i_from_decoder(pkt, dec)) + + def _uper_encode_into(self, enc, pkt, value=None): + # type: (UPER_Encoder, ASN1_Packet, Any) -> None + if value is None: + value = getattr(pkt, self.name) + if value is None: + enc.append_length_determinant(0) + return + enc.append_length_determinant(len(value)) + for item in value: + self.fld._uper_encode_into(enc, pkt, item) + def m2i(self, pkt, # type: ASN1_Packet s, # type: bytes @@ -655,6 +823,18 @@ def m2i(self, if len(s) > 0: raise OER_Decoding_Error("unexpected remainder", remaining=s) return lst, b"" + if pkt.ASN1_codec == ASN1_Codecs.PER: + dec = UPER_Decoder(s) + count, _ = UPER_count_dec(b"", dec=dec) + lst = [] + for _ in range(count): + c, _ = self._extract_packet_from_decoder(dec, pkt) + if c: + lst.append(c) + if UPER_has_unexpected_remainder(dec): + raise UPER_Decoding_Error("unexpected remainder", + remaining=dec.remaining()) + return lst, b"" diff_tag, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag, @@ -685,10 +865,21 @@ def build(self, pkt): s = b"" if pkt.ASN1_codec == ASN1_Codecs.OER: s = OER_unsigned_integer_enc(0) + elif pkt.ASN1_codec == ASN1_Codecs.PER: + enc = UPER_Encoder() + enc.append_length_determinant(0) + s = enc.as_bytes() else: - s = b"".join(self.fld._encode_item(pkt, i) for i in val) - if pkt.ASN1_codec == ASN1_Codecs.OER: - s = OER_unsigned_integer_enc(len(val)) + s + if pkt.ASN1_codec == ASN1_Codecs.PER: + enc = UPER_Encoder() + enc.append_length_determinant(len(val)) + for item in val: + self.fld._uper_encode_into(enc, pkt, item) + s = enc.as_bytes() + else: + s = b"".join(self.fld._encode_item(pkt, i) for i in val) + if pkt.ASN1_codec == ASN1_Codecs.OER: + s = OER_unsigned_integer_enc(len(val)) + s return self.i2m(pkt, s) def i2repr(self, pkt, x): @@ -759,6 +950,10 @@ def dissect(self, pkt, s): self._field.set_val(pkt, None) return s + def dissect_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, UPER_Decoder) -> None + return self._field.dissect_from_decoder(pkt, dec) + def build(self, pkt): # type: (ASN1_Packet) -> bytes if self._field.is_empty(pkt): @@ -773,6 +968,18 @@ def i2repr(self, pkt, x): # type: (ASN1_Packet, Any) -> str return self._field.i2repr(pkt, x) + def set_val(self, pkt, val): + # type: (ASN1_Packet, Any) -> None + self._field.set_val(pkt, val) + + def is_empty(self, pkt): + # type: (ASN1_Packet) -> bool + return self._field.is_empty(pkt) + + def _uper_encode_into(self, enc, pkt, value=None): + # type: (UPER_Encoder, ASN1_Packet, Optional[Any]) -> None + self._field._uper_encode_into(enc, pkt, value) + class ASN1F_omit(ASN1F_field[None, None]): """ @@ -815,6 +1022,7 @@ def __init__(self, name, default, *args, **kwargs): self.default = default self.current_choice = None self.choices = {} # type: Dict[int, _CHOICE_T] + self.choice_order = [] # type: List[int] self.pktchoices = {} for p in args: if hasattr(p, "ASN1_root"): @@ -822,18 +1030,24 @@ def __init__(self, name, default, *args, **kwargs): # should be ASN1_Packet if hasattr(p.ASN1_root, "choices"): root = cast(ASN1F_CHOICE, p.ASN1_root) - for k, v in root.choices.items(): - # ASN1F_CHOICE recursion - self.choices[k] = v + for k in root.choice_order: + self.choices[k] = root.choices[k] + self.choice_order.append(k) else: - self.choices[p.ASN1_root.network_tag] = p + tag = p.ASN1_root.network_tag + self.choices[tag] = p + self.choice_order.append(tag) elif hasattr(p, "ASN1_tag"): if isinstance(p, type): # should be ASN1F_field class - self.choices[int(p.ASN1_tag)] = p + tag = int(p.ASN1_tag) + self.choices[tag] = p + self.choice_order.append(tag) else: # should be ASN1F_field instance - self.choices[p.network_tag] = p + tag = p.network_tag + self.choices[tag] = p + self.choice_order.append(tag) self.pktchoices[hash(p.cls)] = (p.implicit_tag, p.explicit_tag) # noqa: E501 else: raise ASN1_Error("ASN1F_CHOICE: no tag found for one field") @@ -850,6 +1064,13 @@ def m2i(self, pkt, s): _, s = OER_tagging_dec(s, hidden_tag=self.ASN1_tag, explicit_tag=self.explicit_tag) tag, payload = OER_id_dec(s) + elif pkt.ASN1_codec == ASN1_Codecs.PER: + dec = UPER_Decoder(s) + val = self.m2i_from_decoder(pkt, dec) + if UPER_has_unexpected_remainder(dec): + raise UPER_Decoding_Error("unexpected remainder", + remaining=dec.remaining()) + return val, b"" else: _, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, explicit_tag=self.explicit_tag) @@ -890,10 +1111,67 @@ def _choice_tag_for(self, x): return tag return None + def _choice_index_for(self, x): + # type: (Any) -> Optional[int] + tag = self._choice_tag_for(x) + if tag is None: + return None + try: + return self.choice_order.index(tag) + except ValueError: + return None + + def _choice_for_index(self, index): + # type: (int) -> _CHOICE_T + return self.choices[self.choice_order[index]] + + def m2i_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, UPER_Decoder) -> ASN1_Object[Any] + index, _ = UPER_choice_index_dec(b"", len(self.choice_order), dec=dec) + if index >= len(self.choice_order): + raise ASN1_Error( + "ASN1F_CHOICE: unexpected index %s in '%s'" % + (index, self.name) + ) + choice = self._choice_for_index(index) + if hasattr(choice, "ASN1_root"): + raise ASN1_Error( + "ASN1F_CHOICE: UPER packet choices are not supported yet" + ) + if isinstance(choice, type): + return cast( + ASN1_Object[Any], + choice(self.name, b"").m2i_from_decoder(pkt, dec), + ) + return cast(ASN1_Object[Any], choice.m2i_from_decoder(pkt, dec)) + + def _uper_encode_into(self, enc, pkt, value=None): + # type: (UPER_Encoder, ASN1_Packet, Any) -> None + if value is None: + value = getattr(pkt, self.name) + index = self._choice_index_for(value) + if index is None: + raise ASN1_Error( + "ASN1F_CHOICE: cannot encode unknown alternative in '%s'" % + self.name + ) + UPER_choice_index_enc(index, len(self.choice_order), enc=enc) + choice = self._choice_for_index(index) + if hasattr(choice, "ASN1_root"): + cast("ASN1_Packet", value).ASN1_root._uper_encode_into(enc, value) + elif isinstance(choice, type): + choice(self.name, b"")._uper_encode_into(enc, pkt, value) + else: + choice._uper_encode_into(enc, pkt, value) + def i2m(self, pkt, x): # type: (ASN1_Packet, Any) -> bytes if x is None: s = b"" + elif pkt.ASN1_codec == ASN1_Codecs.PER: + enc = UPER_Encoder() + self._uper_encode_into(enc, pkt, x) + s = enc.as_bytes() else: if isinstance(x, ASN1_Object): s = x.enc(pkt.ASN1_codec) @@ -912,6 +1190,8 @@ def i2m(self, pkt, x): explicit_tag=exp) if pkt.ASN1_codec == ASN1_Codecs.OER: return cast(bytes, OER_tagging_enc(s, explicit_tag=self.explicit_tag)) + if pkt.ASN1_codec == ASN1_Codecs.PER: + return s return BER_tagging_enc(s, explicit_tag=self.explicit_tag) def randval(self): diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index aa1fc734f95..5a3674e7e5c 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -255,3 +255,159 @@ __import__('test.scapy.layers.oer_packets', fromlist=['check_oer_field_sequence_ __import__('test.scapy.layers.oer_packets', fromlist=['check_oer_field_choice']).check_oer_field_choice() = OER packet record __import__('test.scapy.layers.oer_packets', fromlist=['check_oer_packet_record']).check_oer_packet_record() + ++ ASN.1 UPER codec += UPER boolean true +UPERcodec_BOOLEAN.enc(1) == b"\x80" += UPER boolean false +UPERcodec_BOOLEAN.enc(0) == b"\x00" += UPER unconstrained integer +UPERcodec_INTEGER.enc(42) == b"\x01*" += UPER constrained integer +UPERcodec_INTEGER.enc(200, uper_min=0, uper_max=255) == b"\xc8" += UPER signed constrained integer +UPERcodec_INTEGER.enc(-1, uper_min=-128, uper_max=127) == b"\x7f" += UPER octet string +UPERcodec_STRING.enc(b"AB") == b"\x02AB" += UPER fixed octet string +UPERcodec_STRING.enc(b"\x12\x34\x56", size_len=3) == b"\x12\x34\x56" += UPER null +UPERcodec_NULL.enc(None) == b"" += UPER enumerated index +UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]) == b"\x80" += UPER bit string variable size +UPERcodec_BIT_STRING.enc((b"\xab\xcd", 16), uper_min=1, uper_max=20) == bytes.fromhex("7d5e68") += UPER enumerated roundtrip +x, r = UPERcodec_ENUMERATED.do_dec(UPERcodec_ENUMERATED.enc(200, uper_enum_values=[1, 200]), uper_enum_values=[1, 200]) +x.val == 200 and r == b"" += UPER integer roundtrip +x, r = UPERcodec_INTEGER.do_dec(UPERcodec_INTEGER.enc(-1)) +x.val == -1 and r == b"" += UPER boolean roundtrip +x, r = UPERcodec_BOOLEAN.do_dec(UPERcodec_BOOLEAN.enc(1)) +x.val == 1 and r == b"" += UPER ASN1 object encoding +ASN1_INTEGER(42).enc(ASN1_Codecs.PER) == b"\x01*" += UPER codec registration +ASN1_Class_UNIVERSAL.INTEGER.get_codec(ASN1_Codecs.PER) is UPERcodec_INTEGER += asn1tools availability probe for UPER +from test.scapy.layers.uper_iop import require_asn1tools +HAS_ASN1TOOLS_UPER = require_asn1tools() + ++ ASN.1 UPER codec roundtrips += UPER codec primitive roundtrips +__import__('test.scapy.layers.uper_codec', fromlist=['check_uper_codec_roundtrips']).check_uper_codec_roundtrips() += UPER codec asn1tools decode interop +(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_codec', fromlist=['check_uper_codec_asn1tools_decode']).check_uper_codec_asn1tools_decode() += UPER codec scapy encode asn1tools decode +(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_codec', fromlist=['check_uper_codec_scapy_decode_asn1tools']).check_uper_codec_scapy_decode_asn1tools() += UPER codec OID encode interop +(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_codec', fromlist=['check_uper_codec_oid_encode_interop']).check_uper_codec_oid_encode_interop() += UPER codec OID roundtrip +__import__('test.scapy.layers.uper_codec', fromlist=['check_uper_codec_oid_roundtrip']).check_uper_codec_oid_roundtrip() + ++ ASN.1 UPER helpers += UPER length determinant +__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_length_determinant']).check_uper_length_determinant() += UPER count roundtrip +__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_count_roundtrip']).check_uper_count_roundtrip() += UPER choice index roundtrip +__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_choice_index_roundtrip']).check_uper_choice_index_roundtrip() += UPER optional presence +__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_optional_presence']).check_uper_optional_presence() += UPER constrained integer helper +__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_constrained_integer']).check_uper_constrained_integer() += UPER constrained signed integer helper +__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_constrained_signed_integer']).check_uper_constrained_signed_integer() += UPER octet string helper roundtrip +__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_octet_string_roundtrip']).check_uper_octet_string_roundtrip() += UPER unexpected remainder detection +__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_has_unexpected_remainder']).check_uper_has_unexpected_remainder() += UPER join encodings +__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_join_encodings']).check_uper_join_encodings() += UPER chained encode into +__import__('test.scapy.layers.uper_helpers', fromlist=['check_uper_chained_encode_into']).check_uper_chained_encode_into() + ++ ASN.1 UPER interoperability (asn1tools) += UPER primitive encode interop with asn1tools +(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_iop', fromlist=['check_primitive_interop']).check_primitive_interop() += UPER composite encode interop with asn1tools +(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_iop', fromlist=['check_composite_interop']).check_composite_interop() += UPER composite decode interop with asn1tools +(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_iop', fromlist=['check_composite_decode_interop']).check_composite_decode_interop() += UPER packet asn1tools interop +(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_iop', fromlist=['check_packet_asn1tools_interop']).check_packet_asn1tools_interop() += UPER packet decode vectors +__import__('test.scapy.layers.uper_iop', fromlist=['check_packet_decode_vectors']).check_packet_decode_vectors() + ++ ASN.1 UPER asn1scc interoperability += asn1scc vector encode interop with asn1tools +(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_asn1scc_iop', fromlist=['check_asn1scc_vectors']).check_asn1scc_vectors() += asn1scc README Message uPER reference (asn1tools) +(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_asn1scc_iop', fromlist=['check_asn1scc_readme_message_reference']).check_asn1scc_readme_message_reference() += asn1scc README MessagePrefix Scapy interop +(not HAS_ASN1TOOLS_UPER) or __import__('test.scapy.layers.uper_asn1scc_iop', fromlist=['check_asn1scc_readme_message_prefix']).check_asn1scc_readme_message_prefix() + ++ ASN.1 UPER packets and fields += UPER field fixed size +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_fixed_size']).check_uper_field_fixed_size() += UPER field integer +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_integer']).check_uper_field_integer() += UPER field boolean +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_boolean']).check_uper_field_boolean() += UPER field string +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_string']).check_uper_field_string() += UPER field constrained integer +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_constrained_integer']).check_uper_field_constrained_integer() += UPER field optional +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_optional']).check_uper_field_optional() += UPER field sequence of +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_sequence_of']).check_uper_field_sequence_of() += UPER field choice +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_choice']).check_uper_field_choice() += UPER field choice definition order +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_choice_definition_order']).check_uper_field_choice_definition_order() += UPER packet record +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_packet_record']).check_uper_packet_record() += UPER field enumerated +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_enumerated']).check_uper_field_enumerated() += UPER field bit string +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_field_bit_string']).check_uper_field_bit_string() += UPER message prefix +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_message_prefix']).check_uper_message_prefix() += UPER sequence with choice +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_sequence_with_choice']).check_uper_sequence_with_choice() += UPER null packet +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_null_packet']).check_uper_null_packet() += UPER variable octet string +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_variable_octet_string']).check_uper_variable_octet_string() += UPER constrained range integer +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_constrained_range_integer']).check_uper_constrained_range_integer() += UPER sequence with enumerated +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_sequence_with_enumerated']).check_uper_sequence_with_enumerated() += UPER sequence of strings +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_sequence_of_strings']).check_uper_sequence_of_strings() += UPER sequence choice hex +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_sequence_choice_hex']).check_uper_sequence_choice_hex() += UPER nested sequence +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_nested_sequence']).check_uper_nested_sequence() += UPER sequence with null +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_sequence_with_null']).check_uper_sequence_with_null() += UPER fixed bit string packet +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_fixed_bit_string']).check_uper_fixed_bit_string() += UPER multi optional +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_multi_optional']).check_uper_multi_optional() += UPER sequence of constrained integers +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_sequence_of_constrained_ints']).check_uper_sequence_of_constrained_ints() += UPER signed integer field +__import__('test.scapy.layers.uper_packets', fromlist=['check_uper_signed_integer']).check_uper_signed_integer() + ++ ASN.1 UPER fuzzing += UPER fuzz encode +__import__('test.scapy.layers.uper_fuzz', fromlist=['check_uper_fuzz_encode']).check_uper_fuzz_encode() += UPER fuzz encode roundtrip +__import__('test.scapy.layers.uper_fuzz', fromlist=['check_uper_fuzz_roundtrip']).check_uper_fuzz_roundtrip() += UPER fuzz codec decode +__import__('test.scapy.layers.uper_fuzz', fromlist=['check_uper_fuzz_codec_decode']).check_uper_fuzz_codec_decode() += UPER fuzz packet decode +__import__('test.scapy.layers.uper_fuzz', fromlist=['check_uper_fuzz_packet_decode']).check_uper_fuzz_packet_decode() diff --git a/test/scapy/layers/uper_asn1scc_iop.py b/test/scapy/layers/uper_asn1scc_iop.py new file mode 100644 index 00000000000..f35e4b59440 --- /dev/null +++ b/test/scapy/layers/uper_asn1scc_iop.py @@ -0,0 +1,261 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +UPER interoperability vectors from ESA asn1scc test cases. + +asn1scc (https://github.com/esa/asn1scc) primarily validates C/Ada code generation +with ACN custom encodings. Portable uPER vectors are taken from v4Tests where +``--TCLS MyPDU[]`` selects standard uPER (empty ACN = default PER). + +Cases that need REAL, explicit APPLICATION tags, or ACN overrides are not +compared against Scapy encoders here (or are asn1tools reference only). +""" + +from typing import Any, List, Tuple + +try: + import asn1tools + HAS_ASN1TOOLS = True +except ImportError: + asn1tools = None # type: ignore + HAS_ASN1TOOLS = False + +from scapy.asn1.uper import ( + UPER_Encoder, + UPER_choice_index_enc, + UPERcodec_BIT_STRING, + UPERcodec_BOOLEAN, + UPERcodec_ENUMERATED, + UPERcodec_INTEGER, + UPERcodec_NULL, + UPERcodec_STRING, +) + +# asn1scc v4Tests/test-cases/acn/05-BOOLEAN/001.asn1 +BOOLEAN_SPEC = ( + "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " + "MyPDU ::= BOOLEAN " + "END" +) + +# asn1scc v4Tests/test-cases/acn/18-NULL/001.asn1 +NULL_SPEC = ( + "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " + "MyPDU ::= NULL " + "END" +) + +# asn1scc v4Tests/test-cases/acn/06-OCTET-STRING/001.asn1 +OCTET_STRING_VAR_SPEC = ( + "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " + "MyPDU ::= OCTET STRING (SIZE(1..20)) " + "END" +) + +# asn1scc v4Tests/test-cases/acn/09-CHOICE/001.asn1 (pdu1 = int1 : 10) +CHOICE_SPEC = ( + "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " + "MyPDU ::= CHOICE { " + "int1 INTEGER(0..15), " + "int2 INTEGER(0..65535), " + "enm ENUMERATED { one(1), two(2), three(3), four(4), thousand(1000) }, " + "buf OCTET STRING (SIZE(10)), " + "gg SEQUENCE { " + "int1 INTEGER(0..15), " + "int2 INTEGER(0..65535), " + "enm ENUMERATED { pone(1), ptwo(2), pthree(3), pfour(4), pthousand(1000) }, " + "buf [APPLICATION 104] OCTET STRING (SIZE(10)) " + "} " + "} " + "END" +) + +# asn1scc v4Tests/test-cases/acn/04-ENUMERATED/001.asn1 (pdu1 = beta) +ENUMERATED_SPEC = ( + "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " + "MyPDU ::= ENUMERATED { alpha(1), beta(200) } " + "END" +) + +# asn1scc v4Tests/test-cases/acn/08-BIT-STRING/001.asn1 (pdu1 = 'ABCD'H) +BIT_STRING_VAR_SPEC = ( + "TEST-CASE DEFINITIONS AUTOMATIC TAGS::= BEGIN " + "MyPDU ::= BIT STRING (SIZE(1..20)) " + "END" +) + +# asn1scc README.md sample.asn (REAL field; asn1tools reference only) +README_MESSAGE_SPEC = ( + "Sample DEFINITIONS AUTOMATIC TAGS ::= BEGIN " + "Message ::= SEQUENCE { " + "msgId INTEGER, " + "myflag INTEGER, " + "value REAL, " + "szDescription OCTET STRING (SIZE(10)), " + "isReady BOOLEAN " + "} " + "END" +) + +README_MESSAGE_HEX = ( + "010101020980cd191eb851eb851f48656c6c6f576f726c6480" +) + +README_MESSAGE_PREFIX_SPEC = ( + "Sample DEFINITIONS AUTOMATIC TAGS ::= BEGIN " + "MessagePrefix ::= SEQUENCE { " + "msgId INTEGER, " + "myflag INTEGER, " + "szDescription OCTET STRING (SIZE(10)), " + "isReady BOOLEAN " + "} " + "END" +) + +README_MESSAGE_PREFIX_HEX = ( + "0101010248656c6c6f576f726c6480" +) + +ASN1SCC_VECTORS = [ + ( + "05-BOOLEAN/001 pdu1", + BOOLEAN_SPEC, + "MyPDU", + True, + lambda _v: UPERcodec_BOOLEAN.enc(1), + ), + ( + "18-NULL/001 pdu1", + NULL_SPEC, + "MyPDU", + None, + lambda _v: UPERcodec_NULL.enc(None), + ), + ( + "06-OCTET-STRING/001 pdu1", + OCTET_STRING_VAR_SPEC, + "MyPDU", + bytes.fromhex("afbc4583"), + lambda v: UPERcodec_STRING.enc(v, uper_min=1, uper_max=20), + ), + ( + "05-BOOLEAN/001 pdu1 false", + BOOLEAN_SPEC, + "MyPDU", + False, + lambda _v: UPERcodec_BOOLEAN.enc(0), + ), + ( + "04-ENUMERATED/001 pdu1 alpha", + ENUMERATED_SPEC, + "MyPDU", + "alpha", + lambda _v: UPERcodec_ENUMERATED.enc( + 1, uper_enum_values=[1, 200], + ), + ), + ( + "04-ENUMERATED/001 pdu1 beta", + ENUMERATED_SPEC, + "MyPDU", + "beta", + lambda _v: UPERcodec_ENUMERATED.enc( + 200, uper_enum_values=[1, 200], + ), + ), + ( + "09-CHOICE/001 pdu1 int1:10", + CHOICE_SPEC, + "MyPDU", + ("int1", 10), + lambda _v: _encode_choice_int1_10(), + ), + ( + "08-BIT-STRING/001 pdu1 ABCD", + BIT_STRING_VAR_SPEC, + "MyPDU", + (bytes.fromhex("abcd"), 16), + lambda _v: UPERcodec_BIT_STRING.enc( + (bytes.fromhex("abcd"), 16), uper_min=1, uper_max=20, + ), + ), +] + + +def require_asn1tools(): + # type: () -> bool + return HAS_ASN1TOOLS + + +def _encode_choice_int1_10(): + # type: () -> bytes + enc = UPER_Encoder() + UPER_choice_index_enc(0, 5, enc=enc) + UPERcodec_INTEGER.encode_into(enc, 10, uper_min=0, uper_max=15) + return enc.as_bytes() + + +def check_asn1scc_vectors(): + # type: () -> None + if not HAS_ASN1TOOLS: + raise RuntimeError("asn1tools is not installed") + for name, spec, pdu, value, encoder in ASN1SCC_VECTORS: + foo = asn1tools.compile_string(spec, "uper") + expected = foo.encode(pdu, value) + got = encoder(value) + assert got == expected, ( + "%s: expected %s, got %s" % + (name, expected.hex(), got.hex()) + ) + + +def check_asn1scc_readme_message_prefix(): + # type: () -> None + """README sample without REAL; Scapy packet roundtrip vs asn1tools.""" + if not HAS_ASN1TOOLS: + raise RuntimeError("asn1tools is not installed") + from test.scapy.layers.uper_packets import UPERMessagePrefix + from scapy.packet import raw + + foo = asn1tools.compile_string(README_MESSAGE_PREFIX_SPEC, "uper") + value = { + "msgId": 1, + "myflag": 2, + "szDescription": b"HelloWorld", + "isReady": True, + } + expected = foo.encode("MessagePrefix", value) + assert expected.hex() == README_MESSAGE_PREFIX_HEX + + pkt = UPERMessagePrefix( + msgId=1, + myflag=2, + szDescription=b"HelloWorld", + isReady=True, + ) + got = raw(pkt) + assert got == expected + decoded = UPERMessagePrefix(got) + assert decoded.msgId.val == 1 + assert decoded.myflag.val == 2 + assert decoded.szDescription.val == b"HelloWorld" + assert decoded.isReady.val == 1 + + +def check_asn1scc_readme_message_reference(): + # type: () -> None + """README C sample output; Scapy does not encode REAL in UPER yet.""" + if not HAS_ASN1TOOLS: + raise RuntimeError("asn1tools is not installed") + foo = asn1tools.compile_string(README_MESSAGE_SPEC, "uper") + value = { + "msgId": 1, + "myflag": 2, + "value": 3.14, + "szDescription": b"HelloWorld", + "isReady": True, + } + got = foo.encode("Message", value) + assert got.hex() == README_MESSAGE_HEX diff --git a/test/scapy/layers/uper_codec.py b/test/scapy/layers/uper_codec.py new file mode 100644 index 00000000000..d834763c290 --- /dev/null +++ b/test/scapy/layers/uper_codec.py @@ -0,0 +1,186 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +UPER primitive codec roundtrip and decode interoperability tests. +""" + +from typing import Any, Dict, List, Tuple, Type + +try: + import asn1tools + HAS_ASN1TOOLS = True +except ImportError: + asn1tools = None # type: ignore + HAS_ASN1TOOLS = False + +from scapy.asn1.uper import ( + UPERcodec_BIT_STRING, + UPERcodec_BOOLEAN, + UPERcodec_ENUMERATED, + UPERcodec_INTEGER, + UPERcodec_NULL, + UPERcodec_OID, + UPERcodec_STRING, +) + +CodecRoundtrip = Tuple[ + Type[Any], + Any, + Dict[str, Any], + Any, +] + +CODEC_ROUNDTRIPS = [ + (UPERcodec_NULL, None, {}, None), + (UPERcodec_BOOLEAN, 1, {}, 1), + (UPERcodec_BOOLEAN, 0, {}, 0), + (UPERcodec_INTEGER, 42, {}, 42), + (UPERcodec_INTEGER, -1, {}, -1), + (UPERcodec_INTEGER, 68719476736, {}, 68719476736), + (UPERcodec_INTEGER, 200, {"uper_min": 0, "uper_max": 255}, 200), + (UPERcodec_INTEGER, -1, {"uper_min": -128, "uper_max": 127}, -1), + (UPERcodec_INTEGER, 127, {"uper_min": -128, "uper_max": 127}, 127), + (UPERcodec_INTEGER, -128, {"uper_min": -128, "uper_max": 127}, -128), + (UPERcodec_STRING, b"AB", {}, b"AB"), + (UPERcodec_STRING, b"\x12\x34\x56", {"size_len": 3}, b"\x12\x34\x56"), + ( + UPERcodec_STRING, + bytes.fromhex("afbc4583"), + {"uper_min": 1, "uper_max": 20}, + bytes.fromhex("afbc4583"), + ), + (UPERcodec_ENUMERATED, 1, {"uper_enum_values": [1, 200]}, 1), + (UPERcodec_ENUMERATED, 200, {"uper_enum_values": [1, 200]}, 200), + ( + UPERcodec_BIT_STRING, + (bytes.fromhex("abcd"), 16), + {"uper_min": 1, "uper_max": 20}, + "1010101111001101", + ), + ( + UPERcodec_BIT_STRING, + (bytes.fromhex("abcd"), 16), + {"uper_min": 16, "uper_max": 16}, + "1010101111001101", + ), + (UPERcodec_ENUMERATED, 1, {"uper_enum_values": [1]}, 1), +] + +DecodeVector = Tuple[ + str, + Any, + Type[Any], + Dict[str, Any], + Any, +] + +DECODE_VECTORS = [ + ("A", True, UPERcodec_BOOLEAN, {}, 1), + ("A", False, UPERcodec_BOOLEAN, {}, 0), + ("B", 42, UPERcodec_INTEGER, {}, 42), + ("B", -1, UPERcodec_INTEGER, {}, -1), + ("C", 200, UPERcodec_INTEGER, {"uper_min": 0, "uper_max": 255}, 200), + ("Signed", -1, UPERcodec_INTEGER, {"uper_min": -128, "uper_max": 127}, -1), + ("Signed", 127, UPERcodec_INTEGER, {"uper_min": -128, "uper_max": 127}, 127), + ("D", b"AB", UPERcodec_STRING, {}, b"AB"), + ("E", b"\x12\x34\x56", UPERcodec_STRING, {"size_len": 3}, b"\x12\x34\x56"), + ("G", None, UPERcodec_NULL, {}, None), + ( + "H", + "alpha", + UPERcodec_ENUMERATED, + {"uper_enum_values": [1, 200]}, + 1, + ), + ( + "H", + "beta", + UPERcodec_ENUMERATED, + {"uper_enum_values": [1, 200]}, + 200, + ), +] + +ASN1TOOLS_OID_SPEC = ( + "Foo DEFINITIONS AUTOMATIC TAGS ::= BEGIN " + "A ::= OBJECT IDENTIFIER " + "END" +) + + +def require_asn1tools(): + # type: () -> bool + return HAS_ASN1TOOLS + + +def _assert_codec_roundtrip(codec, value, kwargs, expected): + # type: (Type[Any], Any, Dict[str, Any], Any) -> None + data = codec.enc(value, **kwargs) + decoded, _remain = codec.do_dec(data, **kwargs) + assert decoded.val == expected + + +def check_uper_codec_roundtrips(): + # type: () -> None + for codec, value, kwargs, expected in CODEC_ROUNDTRIPS: + _assert_codec_roundtrip(codec, value, kwargs, expected) + + +def check_uper_codec_oid_roundtrip(): + # type: () -> None + import scapy.all # noqa: F401 # loads conf.mib for ASN1_OID + for oid in ("1.2.3", "1.2.840.113549"): + data = UPERcodec_OID.enc(oid) + decoded, remain = UPERcodec_OID.do_dec(data) + assert remain == b"" + assert decoded.val == oid + + +def check_uper_codec_oid_encode_interop(): + # type: () -> None + if not HAS_ASN1TOOLS: + raise RuntimeError("asn1tools is not installed") + foo = asn1tools.compile_string(ASN1TOOLS_OID_SPEC, "uper") + for oid in ("1.2.3", "2.999.3"): + expected = foo.encode("A", oid) + got = UPERcodec_OID.enc(oid) + assert got == expected, ( + "OID %r: expected %s, got %s" % + (oid, expected.hex(), got.hex()) + ) + + +def check_uper_codec_asn1tools_decode(): + # type: () -> None + if not HAS_ASN1TOOLS: + raise RuntimeError("asn1tools is not installed") + from test.scapy.layers.uper_iop import ASN1TOOLS_UPER_SPEC + + foo = asn1tools.compile_string(ASN1TOOLS_UPER_SPEC, "uper") + for typename, value, codec, kwargs, expected in DECODE_VECTORS: + encoded = foo.encode(typename, value) + decoded, _remain = codec.do_dec(encoded, **kwargs) + assert decoded.val == expected, ( + "%s %r: expected %r, got %r" % + (typename, value, expected, decoded.val) + ) + + +def check_uper_codec_scapy_decode_asn1tools(): + # type: () -> None + if not HAS_ASN1TOOLS: + raise RuntimeError("asn1tools is not installed") + from test.scapy.layers.uper_iop import ASN1TOOLS_UPER_SPEC, PRIMITIVE_VECTORS + + foo = asn1tools.compile_string(ASN1TOOLS_UPER_SPEC, "uper") + for typename, value, encoder in PRIMITIVE_VECTORS: + encoded = encoder(value) + asn1_value = foo.decode(typename, encoded) + if typename == "H": + assert asn1_value == value + elif typename == "G": + assert asn1_value is None + else: + assert asn1_value == value diff --git a/test/scapy/layers/uper_fuzz.py b/test/scapy/layers/uper_fuzz.py new file mode 100644 index 00000000000..0475d2ef0a9 --- /dev/null +++ b/test/scapy/layers/uper_fuzz.py @@ -0,0 +1,133 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +UPER fuzzing helpers. + +Exercise UPER encode/decode paths with packet.fuzz() and random payloads. +""" + +import os +import random +from typing import Iterable, Type + +from scapy.asn1.asn1 import ASN1_Codecs, ASN1_Decoding_Error, ASN1_Error +from scapy.asn1.uper import ( + UPER_Decoding_Error, + UPER_Encoding_Error, + UPERcodec_BIT_STRING, + UPERcodec_BOOLEAN, + UPERcodec_ENUMERATED, + UPERcodec_INTEGER, + UPERcodec_NULL, + UPERcodec_OID, + UPERcodec_STRING, +) +from scapy.asn1fields import ( + ASN1F_BOOLEAN, + ASN1F_ENUMERATED, + ASN1F_INTEGER, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, + ASN1F_STRING, + ASN1F_optional, +) +from scapy.asn1packet import ASN1_Packet +from scapy.packet import fuzz, raw + +_UPER_CODEC_CLASSES = ( + UPERcodec_INTEGER, + UPERcodec_BOOLEAN, + UPERcodec_NULL, + UPERcodec_STRING, + UPERcodec_OID, + UPERcodec_ENUMERATED, + UPERcodec_BIT_STRING, +) + +_DECODE_ERRORS = ( + UPER_Decoding_Error, + UPER_Encoding_Error, + ASN1_Decoding_Error, + ASN1_Error, + ValueError, + IndexError, +) + + +class UPERFuzzRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_STRING("label", ""), + ASN1F_optional(ASN1F_INTEGER("extra", 0)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ) + + +class UPERFuzzNested(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0), + ASN1F_BOOLEAN("y", False), + ), + ) + + +class UPERFuzzEnumerated(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_ENUMERATED( + "state", 1, {1: "alpha", 200: "beta"}, + ) + + +def _fuzz_packets(): + # type: () -> Iterable[Type[ASN1_Packet]] + return (UPERFuzzRecord, UPERFuzzNested, UPERFuzzEnumerated) + + +def check_uper_fuzz_encode(iterations=25): + # type: (int) -> None + for cls in _fuzz_packets(): + for _ in range(iterations): + try: + data = raw(fuzz(cls())) + except _DECODE_ERRORS: + continue + assert isinstance(data, bytes) + + +def check_uper_fuzz_roundtrip(iterations=25): + # type: (int) -> None + for cls in _fuzz_packets(): + for _ in range(iterations): + try: + cls(raw(fuzz(cls()))) + except _DECODE_ERRORS: + pass + + +def check_uper_fuzz_codec_decode(iterations=100): + # type: (int) -> None + for codec in _UPER_CODEC_CLASSES: + for _ in range(iterations): + data = os.urandom(random.randint(0, 64)) + try: + codec.safedec(data) + except _DECODE_ERRORS: + pass + + +def check_uper_fuzz_packet_decode(iterations=100): + # type: (int) -> None + for cls in _fuzz_packets(): + for _ in range(iterations): + data = os.urandom(random.randint(0, 128)) + try: + cls(data) + except _DECODE_ERRORS: + pass diff --git a/test/scapy/layers/uper_helpers.py b/test/scapy/layers/uper_helpers.py new file mode 100644 index 00000000000..58cf49aa5ac --- /dev/null +++ b/test/scapy/layers/uper_helpers.py @@ -0,0 +1,122 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +UPER low-level helper and bitstream tests. +""" + +from scapy.asn1.uper import ( + UPER_Decoder, + UPER_Encoder, + UPER_choice_index_dec, + UPER_choice_index_enc, + UPER_constrained_int_dec, + UPER_constrained_int_enc, + UPER_count_dec, + UPER_count_enc, + UPER_has_unexpected_remainder, + UPER_join_encodings, + UPER_octet_string_dec, + UPER_octet_string_enc, + UPER_optional_presence_enc, + UPERcodec_INTEGER, +) + + +def check_uper_length_determinant(): + # type: () -> None + for length, expected in [ + (0, b"\x00"), + (1, b"\x01"), + (127, b"\x7f"), + (128, b"\x80\x80"), + (16383, b"\xbf\xff"), + (16384, b"\xc1"), + ]: + enc = UPER_Encoder() + enc.append_length_determinant(length) + assert enc.as_bytes() == expected + + +def check_uper_count_roundtrip(): + # type: () -> None + for count in [0, 1, 3, 127]: + enc = UPER_Encoder() + UPER_count_enc(count, enc=enc) + got, _ = UPER_count_dec(enc.as_bytes()) + assert got == count + + +def check_uper_choice_index_roundtrip(): + # type: () -> None + for index, choices in [(0, 2), (1, 5), (3, 5)]: + enc = UPER_Encoder() + UPER_choice_index_enc(index, choices, enc=enc) + got, _ = UPER_choice_index_dec(enc.as_bytes(), choices) + assert got == index + + +def check_uper_optional_presence(): + # type: () -> None + enc = UPER_Encoder() + UPER_optional_presence_enc([0, 1, 0], enc=enc) + assert enc.as_bytes() == b"\x40" + + +def check_uper_constrained_integer(): + # type: () -> None + data = UPER_constrained_int_enc(10, 0, 15) + value, remain = UPER_constrained_int_dec(data, 0, 15) + assert value == 10 + assert remain == b"" + + +def check_uper_constrained_signed_integer(): + # type: () -> None + for value, expected in [(0, b"\x80"), (-1, b"\x7f"), (127, b"\xff"), (-128, b"\x00")]: + data = UPER_constrained_int_enc(value, -128, 127) + assert data == expected + decoded, remain = UPER_constrained_int_dec(data, -128, 127) + assert decoded == value + assert remain == b"" + + +def check_uper_octet_string_roundtrip(): + # type: () -> None + for data, minimum, maximum in [ + (b"AB", None, None), + (b"\x12\x34\x56", 3, 3), + (bytes.fromhex("afbc4583"), 1, 20), + ]: + encoded = UPER_octet_string_enc(data, minimum, maximum) + dec = UPER_Decoder(encoded) + decoded, _ = UPER_octet_string_dec(encoded, minimum, maximum, dec=dec) + assert decoded == data + assert not UPER_has_unexpected_remainder(dec) + + +def check_uper_has_unexpected_remainder(): + # type: () -> None + assert UPER_has_unexpected_remainder(UPER_Decoder(b"\x00")) is False + assert UPER_has_unexpected_remainder(UPER_Decoder(b"\x80")) is True + + +def check_uper_join_encodings(): + # type: () -> None + a = UPERcodec_INTEGER.enc(1) + b = UPERcodec_INTEGER.enc(2) + joined = UPER_join_encodings(a, b) + dec = UPER_Decoder(joined) + assert dec.read_unconstrained_whole_number() == 1 + assert dec.read_unconstrained_whole_number() == 2 + + +def check_uper_chained_encode_into(): + # type: () -> None + enc = UPER_Encoder() + UPERcodec_INTEGER.encode_into(enc, 42) + UPERcodec_INTEGER.encode_into(enc, -7) + dec = UPER_Decoder(enc.as_bytes()) + assert dec.read_unconstrained_whole_number() == 42 + assert dec.read_unconstrained_whole_number() == -7 diff --git a/test/scapy/layers/uper_iop.py b/test/scapy/layers/uper_iop.py new file mode 100644 index 00000000000..30b1e56396b --- /dev/null +++ b/test/scapy/layers/uper_iop.py @@ -0,0 +1,251 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +UPER interoperability helpers. + +Cross-check Scapy's UPER codec against asn1tools when available. +""" + +from typing import Any, Callable, List, Optional, Tuple + +try: + import asn1tools + HAS_ASN1TOOLS = True +except ImportError: + asn1tools = None # type: ignore + HAS_ASN1TOOLS = False + +from scapy.asn1.uper import ( + UPERcodec_BOOLEAN, + UPERcodec_ENUMERATED, + UPERcodec_INTEGER, + UPERcodec_NULL, + UPERcodec_STRING, + UPER_Encoder, + UPER_choice_index_enc, +) + + +ASN1TOOLS_UPER_SPEC = ( + "Foo DEFINITIONS AUTOMATIC TAGS ::= " + "BEGIN " + "A ::= BOOLEAN " + "B ::= INTEGER " + "C ::= INTEGER (0..255) " + "Signed ::= INTEGER (-128..127) " + "SeqOfC ::= SEQUENCE OF INTEGER (0..255) " + "ChoiceC ::= CHOICE { a INTEGER (0..15), b OCTET STRING } " + "D ::= OCTET STRING " + "E ::= OCTET STRING (SIZE(3)) " + "G ::= NULL " + "Seq ::= SEQUENCE { id INTEGER, flag BOOLEAN, extra INTEGER OPTIONAL } " + "SeqOf ::= SEQUENCE OF INTEGER " + "Choice ::= CHOICE { a INTEGER, b OCTET STRING } " + "H ::= ENUMERATED { alpha(1), beta(200) } " + "Inner ::= SEQUENCE { x INTEGER, y BOOLEAN } " + "Outer ::= SEQUENCE { id INTEGER, inner Inner } " + "Multi ::= SEQUENCE { id INTEGER, a INTEGER OPTIONAL, b OCTET STRING OPTIONAL } " + "END" +) + +PRIMITIVE_VECTORS = [ + ("A", True, lambda v: UPERcodec_BOOLEAN.enc(1 if v else 0)), + ("A", False, lambda v: UPERcodec_BOOLEAN.enc(1 if v else 0)), + ("B", 42, lambda v: UPERcodec_INTEGER.enc(v)), + ("B", -1, lambda v: UPERcodec_INTEGER.enc(v)), + ("C", 200, lambda v: UPERcodec_INTEGER.enc(v, uper_min=0, uper_max=255)), + ("D", b"AB", lambda v: UPERcodec_STRING.enc(v)), + ("E", b"\x12\x34\x56", lambda v: UPERcodec_STRING.enc(v, size_len=3)), + ("G", None, lambda v: UPERcodec_NULL.enc(None)), + ("H", "beta", lambda v: UPERcodec_ENUMERATED.enc( + 200, uper_enum_values=[1, 200], + )), +] + +COMPOSITE_VECTORS = [ + ("Seq", {"id": 42, "flag": True}), + ("Seq", {"id": 42, "flag": True, "extra": 7}), + ("SeqOf", [1, 2, 3]), + ("SeqOfC", [1, 200, 0]), + ("Choice", ("a", 99)), + ("Choice", ("b", b"AB")), + ("ChoiceC", ("a", 10)), + ("ChoiceC", ("b", b"AB")), +] + +from test.scapy.layers.uper_packets import ( + UPERMultiOptional, + UPERNestedSequence, +) +from scapy.packet import raw + +DECODE_COMPOSITE_VECTORS = [ + ("Seq", {"id": 42, "flag": True}, {"id": 42, "flag": True}), + ( + "Seq", + {"id": 42, "flag": True, "extra": 7}, + {"id": 42, "flag": True, "extra": 7}, + ), + ("SeqOf", [1, 2, 3], [1, 2, 3]), + ("SeqOfC", [1, 200, 0], [1, 200, 0]), + ("Choice", ("a", 99), ("a", 99)), + ("Choice", ("b", b"AB"), ("b", b"AB")), + ("ChoiceC", ("a", 10), ("a", 10)), + ("ChoiceC", ("b", b"AB"), ("b", b"AB")), +] + +DECODE_PACKET_VECTORS = [ + ( + UPERNestedSequence, + {"id": 5, "x": 3, "y": True}, + bytes.fromhex("0105010380"), + ), + ( + UPERMultiOptional, + {"id": 1, "a": 2, "b": b"hi"}, + bytes.fromhex("c0404040809a1a40"), + ), +] + +ASN1TOOLS_PACKET_VECTORS = [ + ( + "Outer", + {"id": 5, "inner": {"x": 3, "y": True}}, + UPERNestedSequence, + {"id": 5, "x": 3, "y": True}, + ), + ( + "Multi", + {"id": 1, "a": 2, "b": b"hi"}, + UPERMultiOptional, + {"id": 1, "a": 2, "b": b"hi"}, + ), +] + + +def require_asn1tools(): + # type: () -> bool + return HAS_ASN1TOOLS + + +def _compile_asn1tools(): + # type: () -> Any + if not HAS_ASN1TOOLS: + raise RuntimeError("asn1tools is not installed") + return asn1tools.compile_string(ASN1TOOLS_UPER_SPEC, "uper") + + +def check_primitive_interop(): + # type: () -> None + foo = _compile_asn1tools() + for typename, value, encoder in PRIMITIVE_VECTORS: + expected = foo.encode(typename, value) + got = encoder(value) + assert got == expected, ( + "%s %r: expected %s, got %s" % + (typename, value, expected.hex(), got.hex()) + ) + + +def check_composite_interop(): + # type: () -> None + foo = _compile_asn1tools() + for typename, value in COMPOSITE_VECTORS: + expected = foo.encode(typename, value) + got = _encode_composite(typename, value) + assert got == expected, ( + "%s %r: expected %s, got %s" % + (typename, value, expected.hex(), got.hex()) + ) + + +def check_composite_decode_interop(): + # type: () -> None + foo = _compile_asn1tools() + for typename, encoded_value, expected in DECODE_COMPOSITE_VECTORS: + data = foo.encode(typename, encoded_value) + got = foo.decode(typename, _encode_composite(typename, encoded_value)) + assert got == expected, ( + "%s %r: expected %r, got %r" % (typename, encoded_value, expected, got) + ) + assert foo.decode(typename, data) == expected + + +def check_packet_asn1tools_interop(): + # type: () -> None + foo = _compile_asn1tools() + for typename, asn1_value, cls, pkt_kwargs in ASN1TOOLS_PACKET_VECTORS: + expected = foo.encode(typename, asn1_value) + got = raw(cls(**pkt_kwargs)) + assert got == expected, ( + "%s: expected %s, got %s" % + (typename, expected.hex(), got.hex()) + ) + decoded = cls(got) + for key, value in pkt_kwargs.items(): + field = getattr(decoded, key) + if value is None: + assert field is None + elif isinstance(value, bool): + assert field.val == (1 if value else 0) + else: + assert field.val == value + + +def check_packet_decode_vectors(): + # type: () -> None + for cls, pkt_kwargs, data in DECODE_PACKET_VECTORS: + decoded = cls(data) + for key, value in pkt_kwargs.items(): + field = getattr(decoded, key) + if isinstance(value, bool): + assert field.val == (1 if value else 0) + else: + assert field.val == value + + +def _encode_composite(typename, value): + # type: (str, Any) -> bytes + enc = UPER_Encoder() + if typename == "Seq": + enc.append_bit(1 if value.get("extra") is not None else 0) + UPERcodec_INTEGER.encode_into(enc, value["id"]) + UPERcodec_BOOLEAN.encode_into(enc, 1 if value["flag"] else 0) + if value.get("extra") is not None: + UPERcodec_INTEGER.encode_into(enc, value["extra"]) + return enc.as_bytes() + if typename == "SeqOf": + enc.append_length_determinant(len(value)) + for item in value: + UPERcodec_INTEGER.encode_into(enc, item) + return enc.as_bytes() + if typename == "SeqOfC": + enc.append_length_determinant(len(value)) + for item in value: + UPERcodec_INTEGER.encode_into( + enc, item, uper_min=0, uper_max=255, + ) + return enc.as_bytes() + if typename == "Choice": + alt, payload = value + index = 0 if alt == "a" else 1 + UPER_choice_index_enc(index, 2, enc=enc) + if alt == "a": + UPERcodec_INTEGER.encode_into(enc, payload) + else: + UPERcodec_STRING.encode_into(enc, payload) + return enc.as_bytes() + if typename == "ChoiceC": + alt, payload = value + index = 0 if alt == "a" else 1 + UPER_choice_index_enc(index, 2, enc=enc) + if alt == "a": + UPERcodec_INTEGER.encode_into( + enc, payload, uper_min=0, uper_max=15, + ) + else: + UPERcodec_STRING.encode_into(enc, payload) + return enc.as_bytes() + raise ValueError("unknown composite type %s" % typename) diff --git a/test/scapy/layers/uper_packets.py b/test/scapy/layers/uper_packets.py new file mode 100644 index 00000000000..3d035d14344 --- /dev/null +++ b/test/scapy/layers/uper_packets.py @@ -0,0 +1,542 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +UPER ASN1_Packet and ASN1F_field tests. +""" + +from scapy.asn1.asn1 import ASN1_Codecs, ASN1_INTEGER, ASN1_STRING +from scapy.asn1fields import ( + ASN1F_BIT_STRING, + ASN1F_BOOLEAN, + ASN1F_CHOICE, + ASN1F_ENUMERATED, + ASN1F_INTEGER, + ASN1F_NULL, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, + ASN1F_STRING, + ASN1F_optional, +) +from scapy.asn1packet import ASN1_Packet +from scapy.packet import raw + + +class UPERFixedFields(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), + ASN1F_STRING("s", "", size_len=3), + ) + + +class UPERIntegerField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0) + + +class UPERBooleanField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_BOOLEAN("b", False) + + +class UPERStringField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_STRING("s", "") + + +class UPERConstrainedInteger(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER( + "n", 0, size_len=1, oer_unsigned=True, + ) + + +class UPEROptionalField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_optional(ASN1F_INTEGER("extra", 0)), + ) + + +class UPERSequenceOfIntegers(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + + +class UPERChoiceField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + +class UPERChoiceStringFirst(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_STRING(b""), ASN1F_STRING, ASN1F_INTEGER, + ) + + +class UPERRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_STRING("label", ""), + ASN1F_optional(ASN1F_INTEGER("extra", 0)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ) + + +class UPEREnumeratedField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_ENUMERATED( + "state", 1, {1: "alpha", 200: "beta"}, + ) + + +class UPERBitStringField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_BIT_STRING( + "bits", "0", uper_min=1, uper_max=20, + ) + + +class UPERMessagePrefix(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("msgId", 0), + ASN1F_INTEGER("myflag", 0), + ASN1F_STRING("szDescription", "", size_len=10), + ASN1F_BOOLEAN("isReady", False), + ) + + +class UPERSequenceWithChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_CHOICE("c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING), + ) + + +class UPERNullPacket(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_NULL("n", None) + + +class UPERVariableOctetString(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_STRING("data", "", uper_min=1, uper_max=20) + + +class UPERConstrainedRangeInt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=15) + + +class UPERSequenceWithEnumerated(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_ENUMERATED("state", 1, {1: "alpha", 200: "beta"}), + ) + + +class UPERSequenceOfStrings(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF("items", [], ASN1F_STRING) + + +class UPERNestedSequence(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0), + ASN1F_BOOLEAN("y", False), + ), + ) + + +class UPERSequenceWithNull(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_NULL("n", None), + ) + + +class UPERFixedBitString(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_BIT_STRING("b", "0", uper_min=16, uper_max=16) + + +class UPERSequenceOfConstrainedInts(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "values", [], ASN1F_INTEGER("item", 0, uper_min=0, uper_max=255), + ) + + +class UPERSignedInteger(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=-128, uper_max=127) + + +class UPERMultiOptional(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_optional(ASN1F_INTEGER("a", 0)), + ASN1F_optional(ASN1F_STRING("b", "")), + ) + + +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) + + +def check_uper_field_fixed_size(): + # type: () -> None + pkt = UPERFixedFields(n=200, s=b"ABC") + assert raw(pkt) == b"\xc8ABC" + decoded = _roundtrip(UPERFixedFields, pkt) + assert decoded.n.val == 200 + assert decoded.s.val == b"ABC" + + +def check_uper_field_integer(): + # type: () -> None + pkt = UPERIntegerField(n=12345) + assert raw(pkt) == bytes.fromhex("023039") + decoded = _roundtrip(UPERIntegerField, pkt) + assert decoded.n.val == 12345 + + +def check_uper_field_boolean(): + # type: () -> None + true_pkt = UPERBooleanField(b=True) + assert raw(true_pkt) == b"\x80" + decoded = _roundtrip(UPERBooleanField, true_pkt) + assert decoded.b.val == 1 + + false_pkt = UPERBooleanField(b=False) + assert raw(false_pkt) == b"\x00" + decoded = _roundtrip(UPERBooleanField, false_pkt) + assert decoded.b.val == 0 + + +def check_uper_field_string(): + # type: () -> None + pkt = UPERStringField(s=b"hi") + assert raw(pkt) == bytes.fromhex("026869") + decoded = _roundtrip(UPERStringField, pkt) + assert decoded.s.val == b"hi" + + +def check_uper_field_constrained_integer(): + # type: () -> None + pkt = UPERConstrainedInteger(n=200) + assert raw(pkt) == b"\xc8" + decoded = _roundtrip(UPERConstrainedInteger, pkt) + assert decoded.n.val == 200 + + +def check_uper_field_optional(): + # type: () -> None + present = UPEROptionalField(id=42, flag=True, extra=7) + assert raw(present) == bytes.fromhex("80954041c0") + decoded = _roundtrip(UPEROptionalField, present) + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.extra.val == 7 + + absent = UPEROptionalField(id=42, flag=True, extra=None) + assert raw(absent) == bytes.fromhex("009540") + decoded = _roundtrip(UPEROptionalField, absent) + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.extra is None + + +def check_uper_field_sequence_of(): + # type: () -> None + pkt = UPERSequenceOfIntegers(values=[1, 2, 3]) + assert raw(pkt) == bytes.fromhex("03010101020103") + decoded = _roundtrip(UPERSequenceOfIntegers, pkt) + assert [x.val for x in decoded.values] == [1, 2, 3] + + empty = UPERSequenceOfIntegers(values=[]) + assert raw(empty) == b"\x00" + decoded = _roundtrip(UPERSequenceOfIntegers, empty) + assert [x.val for x in decoded.values] == [] + + +def check_uper_field_choice(): + # type: () -> None + as_int = UPERChoiceField(c=ASN1_INTEGER(99)) + assert raw(as_int) == bytes.fromhex("00b180") + decoded = _roundtrip(UPERChoiceField, as_int) + assert decoded.c.val == 99 + + as_str = UPERChoiceField(c=ASN1_STRING(b"AB")) + assert raw(as_str) == bytes.fromhex("8120a100") + decoded = _roundtrip(UPERChoiceField, as_str) + assert decoded.c.val == b"AB" + + +def check_uper_field_choice_definition_order(): + # type: () -> None + as_str = UPERChoiceStringFirst(c=ASN1_STRING(b"AB")) + assert raw(as_str) == bytes.fromhex("0120a100") + decoded = _roundtrip(UPERChoiceStringFirst, as_str) + assert decoded.c.val == b"AB" + + as_int = UPERChoiceStringFirst(c=ASN1_INTEGER(99)) + assert raw(as_int) == bytes.fromhex("80b180") + decoded = _roundtrip(UPERChoiceStringFirst, as_int) + assert decoded.c.val == 99 + + +def check_uper_packet_record(): + # type: () -> None + full = UPERRecord( + id=42, + flag=True, + label=b"hi", + extra=7, + values=[1, 2, 3], + ) + assert raw(full) == bytes.fromhex("8095409a1a4041c0c04040408040c0") + decoded = _roundtrip(UPERRecord, full) + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"hi" + assert decoded.extra.val == 7 + assert [x.val for x in decoded.values] == [1, 2, 3] + + pkt = UPERRecord( + id=42, + flag=True, + label=b"AB", + extra=None, + values=[1, 2], + ) + body = bytes.fromhex("0095409050808040404080") + assert raw(pkt) == body + decoded = _roundtrip(UPERRecord, pkt) + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"AB" + assert decoded.extra is None + assert [x.val for x in decoded.values] == [1, 2] + + empty = UPERRecord( + id=1, + flag=False, + label=b"", + extra=None, + values=[], + ) + assert raw(empty) == bytes.fromhex("0080800000") + decoded = _roundtrip(UPERRecord, empty) + assert decoded.id.val == 1 + assert decoded.flag.val == 0 + assert decoded.label.val == b"" + assert decoded.extra is None + assert [x.val for x in decoded.values] == [] + + +def check_uper_field_enumerated(): + # type: () -> None + alpha = UPEREnumeratedField(state=1) + assert raw(alpha) == b"\x00" + decoded = _roundtrip(UPEREnumeratedField, alpha) + assert decoded.state.val == 1 + + beta = UPEREnumeratedField(state=200) + assert raw(beta) == b"\x80" + decoded = _roundtrip(UPEREnumeratedField, beta) + assert decoded.state.val == 200 + + +def check_uper_field_bit_string(): + # type: () -> None + from scapy.asn1.asn1 import ASN1_BIT_STRING + + pkt = UPERBitStringField(bits=ASN1_BIT_STRING("1010101111001101")) + assert raw(pkt) == bytes.fromhex("7d5e68") + decoded = _roundtrip(UPERBitStringField, pkt) + assert decoded.bits.val == "1010101111001101" + + +def check_uper_message_prefix(): + # type: () -> None + pkt = UPERMessagePrefix( + msgId=1, + myflag=2, + szDescription=b"HelloWorld", + isReady=True, + ) + assert raw(pkt) == bytes.fromhex("0101010248656c6c6f576f726c6480") + decoded = _roundtrip(UPERMessagePrefix, pkt) + assert decoded.msgId.val == 1 + assert decoded.myflag.val == 2 + assert decoded.szDescription.val == b"HelloWorld" + assert decoded.isReady.val == 1 + + +def check_uper_sequence_with_choice(): + # type: () -> None + pkt = UPERSequenceWithChoice(id=42, c=ASN1_INTEGER(99)) + body = raw(pkt) + decoded = UPERSequenceWithChoice(body) + assert decoded.id.val == 42 + assert decoded.c.val == 99 + + as_str = UPERSequenceWithChoice(id=1, c=ASN1_STRING(b"AB")) + decoded = UPERSequenceWithChoice(raw(as_str)) + assert decoded.id.val == 1 + assert decoded.c.val == b"AB" + + +def check_uper_null_packet(): + # type: () -> None + pkt = UPERNullPacket() + assert raw(pkt) == b"" + decoded = _roundtrip(UPERNullPacket, pkt) + assert decoded.n is None + + +def check_uper_variable_octet_string(): + # type: () -> None + pkt = UPERVariableOctetString(data=bytes.fromhex("afbc4583")) + assert raw(pkt) == bytes.fromhex("1d7de22c18") + decoded = _roundtrip(UPERVariableOctetString, pkt) + assert decoded.data.val == bytes.fromhex("afbc4583") + + +def check_uper_constrained_range_integer(): + # type: () -> None + pkt = UPERConstrainedRangeInt(n=10) + assert raw(pkt) == b"\xa0" + decoded = _roundtrip(UPERConstrainedRangeInt, pkt) + assert decoded.n.val == 10 + + +def check_uper_sequence_with_enumerated(): + # type: () -> None + pkt = UPERSequenceWithEnumerated(id=1, state=200) + assert raw(pkt) == bytes.fromhex("010180") + decoded = _roundtrip(UPERSequenceWithEnumerated, pkt) + assert decoded.id.val == 1 + assert decoded.state.val == 200 + + alpha = UPERSequenceWithEnumerated(id=7, state=1) + assert raw(alpha) == bytes.fromhex("010700") + decoded = _roundtrip(UPERSequenceWithEnumerated, alpha) + assert decoded.state.val == 1 + + +def check_uper_sequence_of_strings(): + # type: () -> None + pkt = UPERSequenceOfStrings(items=[b"A", b"BC"]) + assert raw(pkt) == bytes.fromhex("020141024243") + decoded = _roundtrip(UPERSequenceOfStrings, pkt) + assert [x.val for x in decoded.items] == [b"A", b"BC"] + + empty = UPERSequenceOfStrings(items=[]) + assert raw(empty) == b"\x00" + decoded = _roundtrip(UPERSequenceOfStrings, empty) + assert [x.val for x in decoded.items] == [] + + +def check_uper_sequence_choice_hex(): + # type: () -> None + """Cross-check against asn1tools composite encoding.""" + pkt = UPERSequenceWithChoice(id=1, c=ASN1_INTEGER(99)) + assert raw(pkt) == bytes.fromhex("010100b180") + decoded = UPERSequenceWithChoice(raw(pkt)) + assert decoded.id.val == 1 + assert decoded.c.val == 99 + + +def check_uper_nested_sequence(): + # type: () -> None + pkt = UPERNestedSequence(id=5, x=3, y=True) + assert raw(pkt) == bytes.fromhex("0105010380") + decoded = _roundtrip(UPERNestedSequence, pkt) + assert decoded.id.val == 5 + assert decoded.x.val == 3 + assert decoded.y.val == 1 + + +def check_uper_sequence_with_null(): + # type: () -> None + pkt = UPERSequenceWithNull(id=1) + assert raw(pkt) == bytes.fromhex("0101") + decoded = _roundtrip(UPERSequenceWithNull, pkt) + assert decoded.id.val == 1 + assert getattr(decoded.n, "val", decoded.n) is None + + +def check_uper_fixed_bit_string(): + # type: () -> None + from scapy.asn1.asn1 import ASN1_BIT_STRING + + pkt = UPERFixedBitString(b=ASN1_BIT_STRING("1010101111001101")) + assert raw(pkt) == bytes.fromhex("abcd") + decoded = _roundtrip(UPERFixedBitString, pkt) + assert decoded.b.val == "1010101111001101" + + +def check_uper_sequence_of_constrained_ints(): + # type: () -> None + pkt = UPERSequenceOfConstrainedInts(values=[1, 200, 0]) + assert raw(pkt) == bytes.fromhex("0301c800") + decoded = _roundtrip(UPERSequenceOfConstrainedInts, pkt) + assert [x.val for x in decoded.values] == [1, 200, 0] + + +def check_uper_signed_integer(): + # type: () -> None + for value, expected in [ + (0, b"\x80"), + (-1, b"\x7f"), + (127, b"\xff"), + (-128, b"\x00"), + ]: + pkt = UPERSignedInteger(n=value) + assert raw(pkt) == expected + decoded = _roundtrip(UPERSignedInteger, pkt) + assert decoded.n.val == value + + +def check_uper_multi_optional(): + # type: () -> None + both = UPERMultiOptional(id=1, a=2, b=b"hi") + assert raw(both) == bytes.fromhex("c0404040809a1a40") + decoded = _roundtrip(UPERMultiOptional, both) + assert decoded.id.val == 1 + assert decoded.a.val == 2 + assert decoded.b.val == b"hi" + + none = UPERMultiOptional(id=1, a=None, b=None) + assert raw(none) == bytes.fromhex("004040") + decoded = _roundtrip(UPERMultiOptional, none) + assert decoded.id.val == 1 + assert decoded.a is None + assert decoded.b is None + + only_a = UPERMultiOptional(id=3, a=9, b=None) + assert raw(only_a) == bytes.fromhex("8040c04240") + decoded = _roundtrip(UPERMultiOptional, only_a) + assert decoded.id.val == 3 + assert decoded.a.val == 9 + assert decoded.b is None diff --git a/tox.ini b/tox.ini index 495672a8e9d..e29e5fbc940 100644 --- a/tox.ini +++ b/tox.ini @@ -34,6 +34,7 @@ deps = coverage[toml] python-can cbor2 + asn1tools scapy-rpc # disabled on windows because they require c++ dependencies # brotli 1.1.0 broken https://github.com/google/brotli/issues/1072 From c8b6cd82e05e30eb1153366819401c57a1bff38c Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 1 Jul 2026 21:39:59 +0200 Subject: [PATCH 03/15] minor fixes AI-Assisted: yes (Cursor AI) --- scapy/asn1/uper.py | 91 ++++++++-------- scapy/asn1fields.py | 247 ++++++++++++++++++++++++++------------------ 2 files changed, 197 insertions(+), 141 deletions(-) diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index cfdf2e97e4b..83d0216e4e4 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -129,7 +129,7 @@ def append_bits(self, data, number_of_bits): # type: (bytes, int) -> None if number_of_bits == 0: return - value = int(binascii.hexlify(data), 16) + value = int.from_bytes(data, "big") value >>= (8 * len(data) - number_of_bits) self.append_non_negative_binary_integer(value, number_of_bits) @@ -219,11 +219,9 @@ def _uper_significant_bit_count(data): if not data: return 0 total = 8 * len(data) - dec = UPER_Decoder(data) - start = dec._read_offset() - bits = dec.value[start:start + total] - end = len(bits) - while end > 0 and bits[end - 1] == "0": + bits = int.from_bytes(data, "big") + end = total + while end > 0 and ((bits >> (total - end)) & 1) == 0: end -= 1 trimmed = total - end if trimmed > 0 and trimmed <= 8: @@ -231,15 +229,33 @@ def _uper_significant_bit_count(data): return total +def _uper_per_bits_to_bytes(bit_value, number_of_bits): + # type: (int, int) -> bytes + if number_of_bits == 0: + return b"" + bitstr = format(bit_value, "0%db" % number_of_bits) + value = "10000000" + bitstr + number_of_alignment_bits = (8 - (number_of_bits % 8)) + if number_of_alignment_bits != 8: + value += "0" * number_of_alignment_bits + hexval = hex(int(value, 2))[4:].rstrip("L") + if len(hexval) % 2: + hexval = "0" + hexval + return binascii.unhexlify(hexval) + + def UPER_append_encoded(enc, data): # type: (UPER_Encoder, bytes) -> None if not data: return - dec = UPER_Decoder(data) - start = dec._read_offset() nbits = _uper_significant_bit_count(data) - for i in range(nbits): - enc.append_bit(int(dec.value[start + i])) + if nbits == 0: + return + total = 8 * len(data) + bits = int.from_bytes(data, "big") + shift = total - nbits + value = (bits >> shift) & ((1 << nbits) - 1) + enc.append_non_negative_binary_integer(value, nbits) def UPER_join_encodings(*parts): @@ -273,8 +289,8 @@ def UPER_has_unexpected_remainder(dec): # type: (UPER_Decoder) -> bool if dec.number_of_bits == 0: return False - offset = dec._read_offset() - return dec.value[offset:].strip("0") != "" + mask = (1 << dec.number_of_bits) - 1 + return (dec._bits & mask) != 0 def UPER_count_dec(s, dec=None): @@ -291,25 +307,31 @@ def UPER_count_dec(s, dec=None): class UPER_Decoder(object): def __init__(self, encoded): # type: (bytes) -> None - self.number_of_bits = 8 * len(encoded) - self.total_number_of_bits = self.number_of_bits + self.total_number_of_bits = 8 * len(encoded) + self.number_of_bits = self.total_number_of_bits if encoded: - value = int(binascii.hexlify(encoded), 16) - value |= (0x80 << self.number_of_bits) - self.value = bin(value)[10:] + self._bits = int.from_bytes(encoded, "big") else: - self.value = "" + self._bits = 0 def _read_offset(self): # type: () -> int return self.total_number_of_bits - self.number_of_bits + def _read_bits_int(self, number_of_bits): + # type: (int) -> int + if number_of_bits == 0: + return 0 + consumed = self._read_offset() + shift = self.total_number_of_bits - consumed - number_of_bits + mask = (1 << number_of_bits) - 1 + return (self._bits >> shift) & mask + def read_bit(self): # type: () -> int if self.number_of_bits == 0: raise UPER_Decoding_Error("UPER_Decoder: out of data") - offset = self._read_offset() - bit = int(self.value[offset]) + bit = self._read_bits_int(1) self.number_of_bits -= 1 return bit @@ -319,32 +341,16 @@ def read_bits(self, number_of_bits): raise UPER_Decoding_Error("UPER_Decoder: out of data") if number_of_bits == 0: return b"" - offset = self._read_offset() - bits = self.value[offset:offset + number_of_bits] + value = self._read_bits_int(number_of_bits) self.number_of_bits -= number_of_bits - value = "10000000" + bits - number_of_alignment_bits = (8 - (number_of_bits % 8)) - if number_of_alignment_bits != 8: - value += "0" * number_of_alignment_bits - hexval = hex(int(value, 2))[4:].rstrip("L") - if len(hexval) % 2: - hexval = "0" + hexval - return binascii.unhexlify(hexval) + return _uper_per_bits_to_bytes(value, number_of_bits) def remaining(self): # type: () -> bytes if self.number_of_bits == 0: return b"" - offset = self._read_offset() - bits = self.value[offset:offset + self.number_of_bits] - value = "10000000" + bits - number_of_alignment_bits = (8 - (self.number_of_bits % 8)) - if number_of_alignment_bits != 8: - value += "0" * number_of_alignment_bits - hexval = hex(int(value, 2))[4:].rstrip("L") - if len(hexval) % 2: - hexval = "0" + hexval - return binascii.unhexlify(hexval) + value = self._read_bits_int(self.number_of_bits) + return _uper_per_bits_to_bytes(value, self.number_of_bits) def read_bytes(self, number_of_bytes): # type: (int) -> bytes @@ -356,10 +362,9 @@ def read_non_negative_binary_integer(self, number_of_bits): raise UPER_Decoding_Error("UPER_Decoder: out of data") if number_of_bits == 0: return 0 - offset = self._read_offset() - bits = self.value[offset:offset + number_of_bits] + value = self._read_bits_int(number_of_bits) self.number_of_bits -= number_of_bits - return int(bits, 2) + return value def read_length_determinant(self): # type: () -> int diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 9a1b150010e..d242b7c7873 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -45,6 +45,7 @@ UPER_Decoding_Error, UPER_Decoder, UPER_Encoder, + UPER_append_encoded, UPER_choice_index_dec, UPER_choice_index_enc, UPER_count_dec, @@ -585,6 +586,9 @@ def __init__(self, *seq, **kwargs): ) self.seq = seq self.islist = len(seq) > 1 + self._optionals = tuple( + f for f in seq if isinstance(f, ASN1F_optional) + ) def __repr__(self): # type: () -> str @@ -599,6 +603,75 @@ def get_fields_list(self): return reduce(lambda x, y: x + y.get_fields_list(), self.seq, []) + def _apply_tagging_dec(self, s, pkt): + # type: (bytes, Any) -> bytes + if pkt.ASN1_codec == ASN1_Codecs.OER: + diff_tag, s = OER_tagging_dec( + s, + hidden_tag=self.ASN1_tag, + implicit_tag=self.implicit_tag, + explicit_tag=self.explicit_tag, + safe=self.flexible_tag, + _fname=pkt.name, + ) + else: + diff_tag, s = BER_tagging_dec( + s, + hidden_tag=self.ASN1_tag, + implicit_tag=self.implicit_tag, + explicit_tag=self.explicit_tag, + safe=self.flexible_tag, + _fname=pkt.name, + ) + if diff_tag is not None: + if self.implicit_tag is not None: + self.implicit_tag = diff_tag + elif self.explicit_tag is not None: + self.explicit_tag = diff_tag + return s + + def _dissect_sequence_children(self, pkt, s): + # type: (Any, bytes) -> bytes + if len(s) == 0: + for obj in self.seq: + obj.set_val(pkt, None) + return s + for obj in self.seq: + try: + s = obj.dissect(pkt, s) + except ASN1F_badsequence: + break + return s + + def _m2i_oer(self, pkt, s): + # type: (Any, bytes) -> Tuple[Any, bytes] + s = self._apply_tagging_dec(s, pkt) + s = self._dissect_sequence_children(pkt, s) + if len(s) > 0: + raise OER_Decoding_Error("unexpected remainder", remaining=s) + return [], b"" + + def _m2i_per(self, pkt, s): + # type: (Any, bytes) -> Tuple[Any, bytes] + dec = UPER_Decoder(s) + self._uper_dissect_from_decoder(pkt, dec) + if UPER_has_unexpected_remainder(dec): + raise UPER_Decoding_Error( + "unexpected remainder", + remaining=dec.remaining(), + ) + return [], b"" + + def _m2i_ber(self, pkt, s): + # type: (Any, bytes) -> Tuple[Any, bytes] + s = self._apply_tagging_dec(s, pkt) + codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) + i, s, remain = codec.check_type_check_len(s) + s = self._dissect_sequence_children(pkt, s) + if len(s) > 0: + raise BER_Decoding_Error("unexpected remainder", remaining=s) + return [], remain + def m2i(self, pkt, s): # type: (Any, bytes) -> Tuple[Any, bytes] """ @@ -610,64 +683,14 @@ def m2i(self, pkt, s): It is discarded by dissect() and should not be missed elsewhere. """ if pkt.ASN1_codec == ASN1_Codecs.OER: - diff_tag, s = OER_tagging_dec(s, hidden_tag=self.ASN1_tag, - implicit_tag=self.implicit_tag, - explicit_tag=self.explicit_tag, - safe=self.flexible_tag, - _fname=pkt.name) - if diff_tag is not None: - if self.implicit_tag is not None: - self.implicit_tag = diff_tag - elif self.explicit_tag is not None: - self.explicit_tag = diff_tag - if len(s) == 0: - for obj in self.seq: - obj.set_val(pkt, None) - else: - for obj in self.seq: - try: - s = obj.dissect(pkt, s) - except ASN1F_badsequence: - break - if len(s) > 0: - raise OER_Decoding_Error("unexpected remainder", remaining=s) - return [], b"" + return self._m2i_oer(pkt, s) if pkt.ASN1_codec == ASN1_Codecs.PER: - dec = UPER_Decoder(s) - self._uper_dissect_from_decoder(pkt, dec) - if UPER_has_unexpected_remainder(dec): - raise UPER_Decoding_Error("unexpected remainder", - remaining=dec.remaining()) - return [], b"" - diff_tag, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, - implicit_tag=self.implicit_tag, - explicit_tag=self.explicit_tag, - safe=self.flexible_tag, - _fname=pkt.name) - if diff_tag is not None: - if self.implicit_tag is not None: - self.implicit_tag = diff_tag - elif self.explicit_tag is not None: - self.explicit_tag = diff_tag - codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) - i, s, remain = codec.check_type_check_len(s) - if len(s) == 0: - for obj in self.seq: - obj.set_val(pkt, None) - else: - for obj in self.seq: - try: - s = obj.dissect(pkt, s) - except ASN1F_badsequence: - break - if len(s) > 0: - raise BER_Decoding_Error("unexpected remainder", remaining=s) - return [], remain + return self._m2i_per(pkt, s) + return self._m2i_ber(pkt, s) def _uper_dissect_from_decoder(self, pkt, dec): # type: (Any, UPER_Decoder) -> None - optionals = [f for f in self.seq if isinstance(f, ASN1F_optional)] - presence = [dec.read_bit() for _ in optionals] + presence = [dec.read_bit() for _ in self._optionals] opt_idx = 0 for obj in self.seq: if isinstance(obj, ASN1F_optional): @@ -702,13 +725,12 @@ def build(self, pkt): def _uper_encode_into(self, enc, pkt, value=None): # type: (UPER_Encoder, ASN1_Packet, Optional[Any]) -> None - optionals = [f for f in self.seq if isinstance(f, ASN1F_optional)] - for opt in optionals: + for opt in self._optionals: enc.append_bit(0 if opt.is_empty(pkt) else 1) for obj in self.seq: if isinstance(obj, ASN1F_optional) and obj.is_empty(pkt): continue - obj._uper_encode_into(enc, pkt, value) + obj._uper_encode_into(enc, pkt) class ASN1F_SET(ASN1F_SEQUENCE): @@ -797,7 +819,10 @@ def _uper_encode_into(self, enc, pkt, value=None): return enc.append_length_determinant(len(value)) for item in value: - self.fld._uper_encode_into(enc, pkt, item) + if self.holds_packets: + UPER_append_encoded(enc, bytes(item)) + else: + self.fld._uper_encode_into(enc, pkt, item) def m2i(self, pkt, # type: ASN1_Packet @@ -874,8 +899,15 @@ def build(self, pkt): enc = UPER_Encoder() enc.append_length_determinant(len(val)) for item in val: - self.fld._uper_encode_into(enc, pkt, item) + if self.holds_packets: + UPER_append_encoded(enc, bytes(item)) + else: + self.fld._uper_encode_into(enc, pkt, item) s = enc.as_bytes() + elif self.holds_packets: + s = b"".join(bytes(i) for i in val) + if pkt.ASN1_codec == ASN1_Codecs.OER: + s = OER_unsigned_integer_enc(len(val)) + s else: s = b"".join(self.fld._encode_item(pkt, i) for i in val) if pkt.ASN1_codec == ASN1_Codecs.OER: @@ -1051,6 +1083,59 @@ def __init__(self, name, default, *args, **kwargs): self.pktchoices[hash(p.cls)] = (p.implicit_tag, p.explicit_tag) # noqa: E501 else: raise ASN1_Error("ASN1F_CHOICE: no tag found for one field") + self._tag_to_index = { + tag: idx for idx, tag in enumerate(self.choice_order) + } + + def _dissect_choice_payload(self, pkt, choice, payload): + # type: (ASN1_Packet, _CHOICE_T, bytes) -> Tuple[ASN1_Object[Any], bytes] + if hasattr(choice, "ASN1_root"): + return self.extract_packet(choice, payload, _underlayer=pkt) # type: ignore + if isinstance(choice, type): + return choice(self.name, b"").m2i(pkt, payload) + return choice.m2i(pkt, payload) + + def _m2i_oer(self, pkt, s): + # type: (ASN1_Packet, bytes) -> Tuple[ASN1_Object[Any], bytes] + _, s = OER_tagging_dec( + s, hidden_tag=self.ASN1_tag, explicit_tag=self.explicit_tag, + ) + tag, payload = OER_id_dec(s) + return self._m2i_tagged(pkt, tag, payload) + + def _m2i_per(self, pkt, s): + # type: (ASN1_Packet, bytes) -> Tuple[ASN1_Object[Any], bytes] + dec = UPER_Decoder(s) + val = self.m2i_from_decoder(pkt, dec) + if UPER_has_unexpected_remainder(dec): + raise UPER_Decoding_Error( + "unexpected remainder", + remaining=dec.remaining(), + ) + return val, b"" + + def _m2i_ber(self, pkt, s): + # type: (ASN1_Packet, bytes) -> Tuple[ASN1_Object[Any], bytes] + _, s = BER_tagging_dec( + s, hidden_tag=self.ASN1_tag, explicit_tag=self.explicit_tag, + ) + tag, _ = BER_id_dec(s) + return self._m2i_tagged(pkt, tag, s) + + def _m2i_tagged(self, pkt, tag, payload): + # type: (ASN1_Packet, int, bytes) -> Tuple[ASN1_Object[Any], bytes] + if tag in self.choices: + choice = self.choices[tag] + elif self.flexible_tag: + choice = ASN1F_field + else: + raise ASN1_Error( + "ASN1F_CHOICE: unexpected field in '%s' " + "(tag %s not in possible tags %s)" % ( + self.name, tag, list(self.choices.keys()) + ) + ) + return self._dissect_choice_payload(pkt, choice, payload) def m2i(self, pkt, s): # type: (ASN1_Packet, bytes) -> Tuple[ASN1_Object[Any], bytes] @@ -1061,41 +1146,10 @@ def m2i(self, pkt, s): if len(s) == 0: raise ASN1_Error("ASN1F_CHOICE: got empty string") if pkt.ASN1_codec == ASN1_Codecs.OER: - _, s = OER_tagging_dec(s, hidden_tag=self.ASN1_tag, - explicit_tag=self.explicit_tag) - tag, payload = OER_id_dec(s) - elif pkt.ASN1_codec == ASN1_Codecs.PER: - dec = UPER_Decoder(s) - val = self.m2i_from_decoder(pkt, dec) - if UPER_has_unexpected_remainder(dec): - raise UPER_Decoding_Error("unexpected remainder", - remaining=dec.remaining()) - return val, b"" - else: - _, s = BER_tagging_dec(s, hidden_tag=self.ASN1_tag, - explicit_tag=self.explicit_tag) - tag, _ = BER_id_dec(s) - payload = s - if tag in self.choices: - choice = self.choices[tag] - else: - if self.flexible_tag: - choice = ASN1F_field - else: - raise ASN1_Error( - "ASN1F_CHOICE: unexpected field in '%s' " - "(tag %s not in possible tags %s)" % ( - self.name, tag, list(self.choices.keys()) - ) - ) - if hasattr(choice, "ASN1_root"): - # we don't want to import ASN1_Packet in this module... - return self.extract_packet(choice, payload, _underlayer=pkt) # type: ignore - elif isinstance(choice, type): - return choice(self.name, b"").m2i(pkt, payload) - else: - # XXX check properly if this is an ASN1F_PACKET - return choice.m2i(pkt, payload) + return self._m2i_oer(pkt, s) + if pkt.ASN1_codec == ASN1_Codecs.PER: + return self._m2i_per(pkt, s) + return self._m2i_ber(pkt, s) def _choice_tag_for(self, x): # type: (Any) -> Optional[int] @@ -1116,10 +1170,7 @@ def _choice_index_for(self, x): tag = self._choice_tag_for(x) if tag is None: return None - try: - return self.choice_order.index(tag) - except ValueError: - return None + return self._tag_to_index.get(tag) def _choice_for_index(self, index): # type: (int) -> _CHOICE_T From c5d1b79b6f53207e53f2b61b491777b87daee1e6 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 1 Jul 2026 21:50:29 +0200 Subject: [PATCH 04/15] minor fixes AI-Assisted: yes (Cursor AI) --- scapy/asn1/ber.py | 8 +++++++- scapy/asn1/oer.py | 34 +++++++++++++++++----------------- scapy/asn1/uper.py | 4 ++-- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/scapy/asn1/ber.py b/scapy/asn1/ber.py index 23899274e4a..1f9a9e75463 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -257,11 +257,14 @@ def BER_tagging_dec(s, # type: bytes def BER_tagging_enc(s, implicit_tag=None, explicit_tag=None): # type: (bytes, Optional[int], Optional[int]) -> bytes + from scapy.config import conf if len(s) > 0: if implicit_tag is not None: s = BER_id_enc(implicit_tag) + s[1:] elif explicit_tag is not None: - s = BER_id_enc(explicit_tag) + BER_len_enc(len(s)) + s + s = BER_id_enc(explicit_tag) + BER_len_enc( + len(s), size=conf.ASN1_default_long_size, + ) + s return s # [ BER classes ] # @@ -629,10 +632,13 @@ class BERcodec_SEQUENCE(BERcodec_Object[Union[bytes, List[BERcodec_Object[Any]]] @classmethod def enc(cls, _ll, size_len=0): # type: (Union[bytes, List[BERcodec_Object[Any]]], Optional[int]) -> bytes + from scapy.config import conf if isinstance(_ll, bytes): ll = _ll else: ll = b"".join(x.enc(cls.codec) for x in _ll) + if not size_len: + size_len = conf.ASN1_default_long_size return chb(int(cls.tag)) + BER_len_enc(len(ll), size=size_len) + ll @classmethod diff --git a/scapy/asn1/oer.py b/scapy/asn1/oer.py index 3bb1c1b75a2..2007a60078a 100644 --- a/scapy/asn1/oer.py +++ b/scapy/asn1/oer.py @@ -540,12 +540,12 @@ def do_dec(cls, oer_unsigned=False, # type: bool ): # type: (...) -> Tuple[ASN1_Object[str], bytes] - l, s = OER_len_dec(s) - if l == 0: + length, s = OER_len_dec(s) + if length == 0: return cls.tag.asn1_object(""), s - if len(s) < l: + if len(s) < length: raise OER_Decoding_Error( - "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), l), + "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), length), remaining=s ) unused_bits = orb(s[0]) @@ -554,10 +554,10 @@ def do_dec(cls, "OERcodec_BIT_STRING: too many unused_bits advertised", remaining=s ) - fs = "".join(binrepr(orb(x)).zfill(8) for x in s[1:l]) + fs = "".join(binrepr(orb(x)).zfill(8) for x in s[1:length]) if unused_bits > 0: fs = fs[:-unused_bits] - return cls.tag.asn1_object(fs), s[l:] + return cls.tag.asn1_object(fs), s[length:] @classmethod def enc(cls, _s, size_len=0): @@ -602,13 +602,13 @@ def do_dec(cls, remaining=s ) return cls.tag.asn1_object(s[:size_len]), s[size_len:] - l, s = OER_len_dec(s) - if len(s) < l: + length, s = OER_len_dec(s) + if len(s) < length: raise OER_Decoding_Error( - "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), l), + "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), length), remaining=s ) - return cls.tag.asn1_object(s[:l]), s[l:] + return cls.tag.asn1_object(s[:length]), s[length:] class OERcodec_NULL(OERcodec_Object[None]): @@ -657,13 +657,13 @@ def do_dec(cls, oer_unsigned=False, # type: bool ): # type: (...) -> Tuple[ASN1_Object[bytes], bytes] - l, s = OER_len_dec(s) - if len(s) < l: + length, s = OER_len_dec(s) + if len(s) < length: raise OER_Decoding_Error( - "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), l), + "%s: Got %i bytes while expecting %i" % (cls.__name__, len(s), length), remaining=s ) - content, t = s[:l], s[l:] + content, t = s[:length], s[length:] lst = [] while content: val, content = BER_num_dec(content) @@ -796,11 +796,11 @@ def do_dec(cls, s, context=None, safe=False, if size_len == 4: raw, remain = s[:4], s[4:] else: - l, remain = OER_len_dec(s) - if len(remain) < l: + length, remain = OER_len_dec(s) + if len(remain) < length: raise OER_Decoding_Error("IP address could not be decoded", remaining=s) - raw, remain = remain[:l], remain[l:] + raw, remain = remain[:length], remain[length:] try: ipaddr_ascii = inet_ntoa(raw) except Exception: diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index 83d0216e4e4..cda2c09109d 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -1022,8 +1022,8 @@ def do_dec(cls, ): # type: (...) -> Tuple[ASN1_Object[bytes], bytes] dec = UPER_Decoder(s) - l = dec.read_length_determinant() - content = dec.read_bytes(l) + length = dec.read_length_determinant() + content = dec.read_bytes(length) lst = [] while content: val, content = BER_num_dec(content) From 7feeff24bcf550f675358099c937d580cd24ec11 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 2 Jul 2026 08:12:07 +0200 Subject: [PATCH 05/15] increase test coverage AI-Assisted: yes (Cursor AI) --- test/scapy/layers/asn1.uts | 54 +++++ test/scapy/layers/asn1_coverage.py | 344 +++++++++++++++++++++++++++++ test/scapy/layers/ber_codec.py | 275 +++++++++++++++++++++++ 3 files changed, 673 insertions(+) create mode 100644 test/scapy/layers/asn1_coverage.py create mode 100644 test/scapy/layers/ber_codec.py diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index 5a3674e7e5c..8f94db57923 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -411,3 +411,57 @@ __import__('test.scapy.layers.uper_fuzz', fromlist=['check_uper_fuzz_roundtrip'] __import__('test.scapy.layers.uper_fuzz', fromlist=['check_uper_fuzz_codec_decode']).check_uper_fuzz_codec_decode() = UPER fuzz packet decode __import__('test.scapy.layers.uper_fuzz', fromlist=['check_uper_fuzz_packet_decode']).check_uper_fuzz_packet_decode() + ++ ASN.1 BER codec += BER error formatting +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_error_str']).check_ber_error_str() += BER length encoding +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_len_enc_dec']).check_ber_len_enc_dec() += BER number encoding +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_num_enc_dec']).check_ber_num_enc_dec() += BER identifier encoding +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_id_enc_dec']).check_ber_id_enc_dec() += BER tagging +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_tagging']).check_ber_tagging() += BER integer codec +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_integer']).check_ber_integer() += BER bit string codec +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_bit_string']).check_ber_bit_string() += BER string and null codec +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_string_and_null']).check_ber_string_and_null() += BER OID codec +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_oid']).check_ber_oid() += BER sequence and set codec +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_sequence_and_set']).check_ber_sequence_and_set() += BER IP address codec +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_ipaddress']).check_ber_ipaddress() += BER object dispatch +__import__('test.scapy.layers.ber_codec', fromlist=['check_ber_object_dispatch']).check_ber_object_dispatch() + ++ ASN.1 codec coverage (UPER/OER/asn1fields) += UPER extended helpers +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_uper_error_str']).check_uper_error_str() += UPER length determinant extended +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_uper_length_determinant_extended']).check_uper_length_determinant_extended() += UPER unconstrained whole number +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_uper_unconstrained_whole_number']).check_uper_unconstrained_whole_number() += UPER bit string paths +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_uper_bit_string_paths']).check_uper_bit_string_paths() += UPER enumerated range +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_uper_enumerated_range']).check_uper_enumerated_range() += UPER sequence errors +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_uper_sequence_errors']).check_uper_sequence_errors() += UPER IP address codec +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_uper_ipaddress']).check_uper_ipaddress() += OER extended coverage +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_oer_error_str']).check_oer_error_str() += OER IP address and sequence +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_oer_ipaddress_and_sequence']).check_oer_ipaddress_and_sequence() += asn1fields enum and flags +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_enum_and_flags']).check_asn1fields_enum_and_flags() += asn1fields encaps and packet +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_encaps_and_packet']).check_asn1fields_encaps_and_packet() += asn1fields choice and special fields +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_choice_and_special']).check_asn1fields_choice_and_special() += asn1fields optional dissect +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_optional_dissect']).check_asn1fields_optional_dissect() diff --git a/test/scapy/layers/asn1_coverage.py b/test/scapy/layers/asn1_coverage.py new file mode 100644 index 00000000000..d03de639f0a --- /dev/null +++ b/test/scapy/layers/asn1_coverage.py @@ -0,0 +1,344 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +Additional coverage for UPER, OER, and asn1fields helpers. +""" + + +def _raises(exc, func): + # type: (type, Any) -> None + try: + func() + except exc: + return + raise AssertionError("Expected %s" % exc.__name__) + + +from typing import Any + +from scapy.asn1.asn1 import ( + ASN1_BIT_STRING, + ASN1_Codecs, + ASN1_INTEGER, + ASN1_STRING, + ASN1_TIME_TICKS, +) +from scapy.asn1.oer import ( + OER_Decoding_Error, + OER_Encoding_Error, + OERcodec_BIT_STRING, + OERcodec_IPADDRESS, + OERcodec_SEQUENCE, + OERcodec_SET, +) +from scapy.asn1.uper import ( + UPER_Decoding_Error, + UPER_Encoding_Error, + UPER_Decoder, + UPER_Encoder, + UPERcodec_BIT_STRING, + UPERcodec_ENUMERATED, + UPERcodec_IPADDRESS, + UPERcodec_SEQUENCE, + UPERcodec_SET, +) +from scapy.asn1fields import ( + ASN1F_BIT_STRING_ENCAPS, + ASN1F_CHOICE, + ASN1F_FLAGS, + ASN1F_IPADDRESS, + ASN1F_INTEGER, + ASN1F_PACKET, + ASN1F_SEQUENCE, + ASN1F_SET_OF, + ASN1F_STRING, + ASN1F_STRING_ENCAPS, + ASN1F_STRING_PacketField, + ASN1F_TIME_TICKS, + ASN1F_enum_INTEGER, + ASN1F_optional, +) +from scapy.asn1packet import ASN1_Packet +from scapy.packet import raw + + +class _InnerRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_enum_INTEGER("mode", ASN1_INTEGER(0), ["off", "on"]), + ) + + +class _EncapsRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_STRING_ENCAPS("payload", None, _InnerRecord), + ) + + +class _FlagsRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_FLAGS("f", "000", ["read", "write", "exec"]), + ) + + +class _SetOfRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SET_OF("items", [], ASN1F_INTEGER) + + +class _PacketFieldRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_STRING_PacketField("data", b""), + ) + + +class _ExplicitPacket(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_PACKET("inner", None, _InnerRecord, explicit_tag=0xA2) + + +class _BitEncapsRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BIT_STRING_ENCAPS("b", None, _InnerRecord), + ) + + +def check_uper_error_str(): + # type: () -> None + obj = ASN1_INTEGER(2) + err = UPER_Encoding_Error("enc", encoded=obj, remaining=b"x") + assert "Already encoded" in str(err) + err2 = UPER_Decoding_Error("dec", decoded=obj, remaining=b"y") + assert "Already decoded" in str(err2) + + +def check_uper_length_determinant_extended(): + # type: () -> None + enc = UPER_Encoder() + assert enc.append_length_determinant(32768) == 32768 + assert enc.as_bytes() == b"\xc2" + + enc = UPER_Encoder() + assert enc.append_length_determinant(49152) == 49152 + assert enc.as_bytes() == b"\xc3" + + enc = UPER_Encoder() + assert enc.append_length_determinant(65535) == 49152 + assert enc.as_bytes() == b"\xc3" + + +def check_uper_unconstrained_whole_number(): + # type: () -> None + enc = UPER_Encoder() + enc.append_unconstrained_whole_number(-256) + dec = UPER_Decoder(enc.as_bytes()) + assert dec.read_unconstrained_whole_number() == -256 + + enc = UPER_Encoder() + enc.append_unconstrained_whole_number(0) + dec = UPER_Decoder(enc.as_bytes()) + assert dec.read_unconstrained_whole_number() == 0 + + +def check_uper_bit_string_paths(): + # type: () -> None + encoded = UPERcodec_BIT_STRING.enc("1010", uper_min=1, uper_max=20) + obj, remain = UPERcodec_BIT_STRING.do_dec( + encoded, uper_min=1, uper_max=20, + ) + assert obj.val == "1010" + + encoded2 = UPERcodec_BIT_STRING.enc(b"\xab", uper_min=4, uper_max=8) + obj2, _ = UPERcodec_BIT_STRING.do_dec(encoded2, uper_min=4, uper_max=8) + assert len(obj2.val) == 8 + + fixed = UPERcodec_BIT_STRING.enc("1010101111001101", uper_min=16, uper_max=16) + obj3, _ = UPERcodec_BIT_STRING.do_dec(fixed, uper_min=16, uper_max=16) + assert obj3.val == "1010101111001101" + + +def check_uper_enumerated_range(): + # type: () -> None + encoded = UPERcodec_ENUMERATED.enc(3, uper_min=0, uper_max=7) + obj, remain = UPERcodec_ENUMERATED.do_dec(encoded, uper_min=0, uper_max=7) + assert obj.val == 3 + assert remain == b"" + + enc = UPER_Encoder() + UPERcodec_ENUMERATED.encode_into(enc, 2, uper_min=0, uper_max=3) + obj2 = UPERcodec_ENUMERATED.dec_from_decoder( + UPER_Decoder(enc.as_bytes()), + uper_min=0, + uper_max=3, + ) + assert obj2.val == 2 + + +def check_uper_sequence_errors(): + # type: () -> None + _raises(UPER_Encoding_Error, lambda: UPERcodec_SEQUENCE.enc([ASN1_INTEGER(1)])) + + _raises(UPER_Decoding_Error, lambda: UPERcodec_SEQUENCE.do_dec(b"\x00")) + + assert UPERcodec_SET.enc(b"raw") == b"raw" + + +def check_uper_ipaddress(): + # type: () -> None + encoded = UPERcodec_IPADDRESS.enc("10.0.0.1") + obj, remain = UPERcodec_IPADDRESS.do_dec(encoded) + assert obj.val == "10.0.0.1" + assert remain == b"" + + _raises(UPER_Encoding_Error, lambda: UPERcodec_IPADDRESS.enc("bad-ip")) + + +def check_oer_error_str(): + # type: () -> None + obj = ASN1_INTEGER(1) + err = OER_Encoding_Error("enc", encoded=obj, remaining=b"z") + assert "Already encoded" in str(err) + err2 = OER_Decoding_Error("dec", decoded=obj, remaining=b"w") + assert "Already decoded" in str(err2) + + +def check_oer_ipaddress_and_sequence(): + # type: () -> None + encoded = OERcodec_IPADDRESS.enc("127.0.0.1") + obj, remain = OERcodec_IPADDRESS.do_dec(encoded) + assert obj.val == "127.0.0.1" + assert remain == b"" + + fixed = OERcodec_IPADDRESS.enc("127.0.0.1", size_len=4) + obj2, remain2 = OERcodec_IPADDRESS.do_dec(fixed, size_len=4) + assert obj2.val == "127.0.0.1" + assert remain2 == b"" + + _raises(OER_Encoding_Error, lambda: OERcodec_IPADDRESS.enc("bad-ip")) + + _raises(OER_Decoding_Error, lambda: OERcodec_IPADDRESS.do_dec(b"\x01")) + + assert OERcodec_SEQUENCE.enc(b"payload") == b"payload" + assert OERcodec_SET.enc(b"payload") == b"payload" + + _raises(OER_Decoding_Error, lambda: OERcodec_SEQUENCE.do_dec(b"\x00")) + + empty, remain = OERcodec_BIT_STRING.do_dec(OERcodec_BIT_STRING.enc("")) + assert empty.val == "" + assert remain == b"" + + +def check_asn1fields_enum_and_flags(): + # type: () -> None + pkt = _InnerRecord(mode="on") + built = raw(pkt) + decoded = _InnerRecord(built) + assert decoded.mode.val == 1 + + flags = _FlagsRecord(f="read+exec") + assert flags.f.val == "101" + assert "read, exec" in _FlagsRecord.ASN1_root.seq[0].i2repr(flags, flags.f) + + set_pkt = _SetOfRecord(items=[ASN1_INTEGER(0), ASN1_INTEGER(1)]) + set_raw = raw(set_pkt) + set_dec = _SetOfRecord(set_raw) + assert [x.val for x in set_dec.items] == [0, 1] + + +def check_asn1fields_encaps_and_packet(): + # type: () -> None + inner = _InnerRecord(mode=1) + enc = _EncapsRecord() + enc.payload = inner + enc_raw = raw(enc) + enc_dec = _EncapsRecord(enc_raw) + assert enc_dec.payload.mode.val == 1 + + pkt_field = _PacketFieldRecord() + pkt_field.data = _InnerRecord(mode=0) + pf_raw = raw(pkt_field) + pf_dec = _PacketFieldRecord(pf_raw) + assert isinstance(pf_dec.data.val, bytes) + + explicit = _ExplicitPacket() + explicit.inner = _InnerRecord(mode=1) + ex_raw = raw(explicit) + ex_dec = _ExplicitPacket(ex_raw) + assert ex_dec.inner.mode.val == 1 + + +def check_asn1fields_choice_and_special(): + # type: () -> None + class _OerChoiceRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + class _BerChoiceRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + oer = _OerChoiceRecord(c=ASN1_INTEGER(1)) + oer_dec = _OerChoiceRecord(raw(oer)) + assert oer_dec.c.val == 1 + + ber = _BerChoiceRecord(c=ASN1_INTEGER(0)) + ber_dec = _BerChoiceRecord(raw(ber)) + assert ber_dec.c.val == 0 + + inner_bytes = raw(_InnerRecord(mode=0)) + bit_payload = ASN1_BIT_STRING( + inner_bytes, + readable=True, + ) + bit_pkt = _BitEncapsRecord(b=bit_payload) + bit_dec = _BitEncapsRecord(raw(bit_pkt)) + assert bit_dec.b.mode.val == 0 + + class _TicksRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_TIME_TICKS("t", ASN1_TIME_TICKS(0)) + + class _IpRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_IPADDRESS("addr", ASN1_STRING(b"")) + + ticks = _TicksRecord(t=ASN1_TIME_TICKS(1234)) + assert raw(ticks).endswith(b"\x04\xd2") + + ip = _IpRecord() + ip.addr = "192.168.1.1" + assert raw(ip) == b"\x40\x04\xc0\xa8\x01\x01" + + +def check_asn1fields_optional_dissect(): + # type: () -> None + class _OptRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_optional(ASN1F_INTEGER("extra", 0)), + ) + + class _BerChoiceRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + pkt = _OptRecord(id=0, extra=None) + assert raw(pkt) + decoded = _OptRecord(raw(pkt)) + assert decoded.extra is None + + choice_rand = _BerChoiceRecord.ASN1_root.randval() + assert choice_rand is not None diff --git a/test/scapy/layers/ber_codec.py b/test/scapy/layers/ber_codec.py new file mode 100644 index 00000000000..e6939f7a27e --- /dev/null +++ b/test/scapy/layers/ber_codec.py @@ -0,0 +1,275 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +BER codec and helper coverage tests. +""" + +from typing import Any + + +def _raises(exc, func): + # type: (type, Any) -> None + try: + func() + except exc: + return + raise AssertionError("Expected %s" % exc.__name__) + + +from scapy.asn1.asn1 import ( + ASN1_Class_UNIVERSAL, + ASN1_DECODING_ERROR, + ASN1_INTEGER, + ASN1_Object, +) +from scapy.asn1.ber import ( + BER_BadTag_Decoding_Error, + BER_Decoding_Error, + BER_Encoding_Error, + BER_Exception, + BER_id_dec, + BER_id_enc, + BER_len_dec, + BER_len_enc, + BER_num_dec, + BER_num_enc, + BER_tagging_dec, + BER_tagging_enc, + BERcodec_BIT_STRING, + BERcodec_INTEGER, + BERcodec_IPADDRESS, + BERcodec_NULL, + BERcodec_Object, + BERcodec_OID, + BERcodec_SEQUENCE, + BERcodec_SET, + BERcodec_STRING, +) +from scapy.config import conf + + +def check_ber_error_str(): + # type: () -> None + obj = ASN1_INTEGER(1) + enc_err = BER_Encoding_Error("enc", encoded=obj, remaining=b"rest") + assert "Already encoded" in str(enc_err) + enc_err2 = BER_Encoding_Error("enc", encoded="raw", remaining=b"") + assert "raw" in str(enc_err2) + + dec_err = BER_Decoding_Error("dec", decoded=obj, remaining=b"tail") + assert "Already decoded" in str(dec_err) + dec_err2 = BER_Decoding_Error("dec", decoded=[1], remaining=b"") + assert "[1]" in str(dec_err2) + + +def check_ber_len_enc_dec(): + # type: () -> None + for value in [0, 1, 127, 128, 999]: + encoded = BER_len_enc(value) + length, remain = BER_len_dec(encoded) + assert length == value + assert remain == b"" + + assert BER_len_enc(45, size=None) == BER_len_enc(45, size=0) + assert BER_len_enc(45, size=4) == b"\x84\x00\x00\x00-" + + _raises(BER_Exception, lambda: BER_len_enc(0, size=128)) + + _raises(BER_Decoding_Error, lambda: BER_len_dec(b"\x82")) + + +def check_ber_num_enc_dec(): + # type: () -> None + for value in [0, 1, 127, 256, 16384]: + encoded = BER_num_enc(value) + decoded, remain = BER_num_dec(encoded) + assert decoded == value + assert remain == b"" + + _raises(BER_Decoding_Error, lambda: BER_num_dec(b"")) + + _raises(BER_Decoding_Error, lambda: BER_num_dec(b"\x80\x80")) + + +def check_ber_id_enc_dec(): + # type: () -> None + for tag in [0x02, 0x30, 0x81, 0xA0]: + encoded = BER_id_enc(tag) + decoded, remain = BER_id_dec(encoded) + assert decoded == tag + assert remain == b"" + + high_tag = (0x03 << 5) + 0x22 + encoded = BER_id_enc(high_tag) + decoded, remain = BER_id_dec(encoded) + assert decoded == high_tag + assert remain == b"" + + +def check_ber_tagging(): + # type: () -> None + inner = BERcodec_INTEGER.enc(7) + implicit = BER_tagging_enc(inner, implicit_tag=0xA0) + assert implicit.startswith(b"\xa0") + real_tag, payload = BER_tagging_dec( + implicit, + hidden_tag=ASN1_Class_UNIVERSAL.INTEGER, + implicit_tag=0xA0, + ) + assert real_tag is None + assert payload[0] == int(ASN1_Class_UNIVERSAL.INTEGER) + + conf.ASN1_default_long_size = 4 + try: + explicit = BER_tagging_enc(inner, explicit_tag=0xA1) + assert explicit.startswith(b"\xa1\x84") + real_tag, payload = BER_tagging_dec( + explicit, + explicit_tag=0xA1, + ) + assert real_tag is None + assert payload == inner + finally: + conf.ASN1_default_long_size = 0 + + _raises(BER_Decoding_Error, lambda: BER_tagging_dec( + implicit, + hidden_tag=ASN1_Class_UNIVERSAL.INTEGER, + implicit_tag=0xA1, + )) + + safe_tag, _ = BER_tagging_dec( + implicit, + hidden_tag=ASN1_Class_UNIVERSAL.INTEGER, + implicit_tag=0xA1, + safe=True, + ) + assert safe_tag == 0xA0 + + +def check_ber_integer(): + # type: () -> None + for value in [0, 1, 127, 128, 255, -1, -128, -129]: + encoded = BERcodec_INTEGER.enc(value) + obj, remain = BERcodec_INTEGER.do_dec(encoded) + assert obj.val == value + assert remain == b"" + + _raises(BER_BadTag_Decoding_Error, lambda: BERcodec_INTEGER.do_dec(BERcodec_STRING.enc(b"x"))) + + _raises(BER_Decoding_Error, lambda: BERcodec_INTEGER.check_type_get_len(b"\x02")) + + +def check_ber_bit_string(): + # type: () -> None + encoded = BERcodec_BIT_STRING.enc("1011") + obj, remain = BERcodec_BIT_STRING.do_dec(encoded) + assert obj.val == "1011" + assert remain == b"" + + padded = BERcodec_BIT_STRING.enc("10110000") + obj2, _ = BERcodec_BIT_STRING.do_dec(padded) + assert obj2.val == "10110000" + + _raises(BER_Decoding_Error, lambda: BERcodec_BIT_STRING.do_dec(b"\x03\x01\x08", safe=True)) + + _raises(BER_Decoding_Error, lambda: BERcodec_BIT_STRING.do_dec(b"\x03\x00")) + + +def check_ber_string_and_null(): + # type: () -> None + encoded = BERcodec_STRING.enc(b"hello") + obj, remain = BERcodec_STRING.do_dec(encoded) + assert obj.val == b"hello" + assert remain == b"" + + null = BERcodec_NULL.enc(0) + assert null == b"\x05\x00" + obj, remain = BERcodec_NULL.do_dec(null) + assert obj.val == 0 + + non_null = BERcodec_NULL.enc(42) + obj, remain = BERcodec_NULL.do_dec(non_null) + assert obj.val == 42 + + +def check_ber_oid(): + # type: () -> None + encoded = BERcodec_OID.enc("1.2.840.113556.1.4.529") + obj, remain = BERcodec_OID.do_dec(encoded) + assert obj.val == "1.2.840.113556.1.4.529" + assert remain == b"" + + empty, remain = BERcodec_OID.do_dec(BERcodec_OID.enc("")) + assert empty.val == "" + assert remain == b"" + + +def check_ber_sequence_and_set(): + # type: () -> None + payload = BERcodec_INTEGER.enc(1) + BERcodec_INTEGER.enc(2) + seq = BERcodec_SEQUENCE.enc(payload) + obj, remain = BERcodec_SEQUENCE.do_dec(seq) + assert len(obj.val) == 2 + assert obj.val[0].val == 1 + assert obj.val[1].val == 2 + assert remain == b"" + + as_list = BERcodec_SEQUENCE.enc([ASN1_INTEGER(3), ASN1_INTEGER(4)]) + obj2, remain2 = BERcodec_SEQUENCE.do_dec(as_list) + assert [x.val for x in obj2.val] == [3, 4] + assert remain2 == b"" + + st = BERcodec_SET.enc(payload) + obj3, remain3 = BERcodec_SET.do_dec(st) + assert len(obj3.val) == 2 + assert remain3 == b"" + + conf.ASN1_default_long_size = 4 + try: + long_seq = BERcodec_SEQUENCE.enc(payload) + assert long_seq.startswith(b"0\x84") + finally: + conf.ASN1_default_long_size = 0 + + _raises(BER_Decoding_Error, lambda: BERcodec_SEQUENCE.do_dec(b"\x30\x05" + BERcodec_INTEGER.enc(1))) + + +def check_ber_ipaddress(): + # type: () -> None + encoded = BERcodec_IPADDRESS.enc("192.168.0.1") + obj, remain = BERcodec_IPADDRESS.do_dec(encoded) + assert obj.val == "192.168.0.1" + assert remain == b"" + + _raises(BER_Encoding_Error, lambda: BERcodec_IPADDRESS.enc("not-an-ip")) + + _raises(BER_Decoding_Error, lambda: BERcodec_IPADDRESS.do_dec(BERcodec_STRING.enc(b"bad"))) + + +def check_ber_object_dispatch(): + # type: () -> None + encoded = BERcodec_INTEGER.enc(99) + obj, remain = BERcodec_Object.do_dec(encoded) + assert obj.val == 99 + assert remain == b"" + + _raises(BER_Decoding_Error, lambda: BERcodec_Object.check_string(b"")) + + _raises(BER_Decoding_Error, lambda: BERcodec_Object.do_dec(b"\xff\x00")) + + bad, remain = BERcodec_Object.safedec(b"\x02\x01\x01") + assert isinstance(bad, ASN1_INTEGER) + assert bad.val == 1 + + unknown, remain = BERcodec_Object.safedec(b"\xff\x00") + assert isinstance(unknown, ASN1_DECODING_ERROR) + + truncated, remain = BERcodec_Object.dec(b"\x02\x05\x01", safe=True) + assert isinstance(truncated, ASN1_DECODING_ERROR) + assert remain == b"" + + _raises(TypeError, lambda: BERcodec_Object.enc(object())) + assert BERcodec_Object.enc("42") == BERcodec_STRING.enc("42") From 4f1be1e722b9f79940fadce4bad487dde3970e67 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 2 Jul 2026 20:49:20 +0200 Subject: [PATCH 06/15] minor fixes AI-Assisted: yes (Cursor AI) --- scapy/asn1/uper.py | 10 ++- scapy/asn1fields.py | 192 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 170 insertions(+), 32 deletions(-) diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index cda2c09109d..f6ae7de82e0 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -808,7 +808,15 @@ def encode_into(cls, if size_len: minimum = maximum = size_len if minimum is not None and maximum is not None and minimum == maximum: - enc.append_bits(s, nbits) + if nbits >= minimum: + value = int.from_bytes(s, "big") >> (8 * len(s) - minimum) + elif isinstance(_s, str) and _s and all(c in "01" for c in _s): + value = int(_s, 2) + elif nbits > 0: + value = int.from_bytes(s, "big") >> max(0, 8 * len(s) - nbits) + else: + value = 0 + enc.append_non_negative_binary_integer(value, minimum) elif minimum is not None and maximum is not None: enc.append_non_negative_binary_integer( nbits - minimum, UPER_bits_for_range(maximum - minimum) diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index d242b7c7873..a30f9d7f8d9 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -46,8 +46,10 @@ UPER_Decoder, UPER_Encoder, UPER_append_encoded, + UPER_bits_for_range, UPER_choice_index_dec, UPER_choice_index_enc, + UPER_constrained_int_enc, UPER_count_dec, UPER_has_unexpected_remainder, ) @@ -579,6 +581,7 @@ class ASN1F_SEQUENCE(ASN1F_field[List[Any], List[Any]]): def __init__(self, *seq, **kwargs): # type: (*Any, **Any) -> None + self.uper_extensible = kwargs.pop("uper_extensible", False) name = "dummy_seq_name" default = [field.default for field in seq] super(ASN1F_SEQUENCE, self).__init__( @@ -690,6 +693,11 @@ def m2i(self, pkt, s): def _uper_dissect_from_decoder(self, pkt, dec): # type: (Any, UPER_Decoder) -> None + if self.uper_extensible: + if dec.read_bit(): + raise UPER_Decoding_Error( + "ASN1F_SEQUENCE: extension additions are not supported" + ) presence = [dec.read_bit() for _ in self._optionals] opt_idx = 0 for obj in self.seq: @@ -725,6 +733,8 @@ def build(self, pkt): def _uper_encode_into(self, enc, pkt, value=None): # type: (UPER_Encoder, ASN1_Packet, Optional[Any]) -> None + if self.uper_extensible: + enc.append_bit(0) for opt in self._optionals: enc.append_bit(0 if opt.is_empty(pkt) else 1) for obj in self.seq: @@ -760,6 +770,9 @@ def __init__(self, context=None, # type: Optional[Any] implicit_tag=None, # type: Optional[Any] explicit_tag=None, # type: Optional[Any] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + uper_extensible=False, # type: bool ): # type: (...) -> None if isinstance(cls, type) and issubclass(cls, ASN1F_field) or \ @@ -782,6 +795,28 @@ def __init__(self, implicit_tag=implicit_tag, explicit_tag=explicit_tag ) self.default = default + self.uper_min = uper_min + self.uper_max = uper_max + self.uper_extensible = uper_extensible + + def _uper_count_enc(self, enc, count): + # type: (UPER_Encoder, int) -> None + if self.uper_min is not None and self.uper_max is not None: + UPER_constrained_int_enc(count, self.uper_min, self.uper_max, enc=enc) + else: + enc.append_length_determinant(count) + + def _uper_count_dec(self, dec): + # type: (UPER_Decoder) -> int + if self.uper_min is not None and self.uper_max is not None: + size = self.uper_max - self.uper_min + return cast( + int, + dec.read_non_negative_binary_integer( + UPER_bits_for_range(size), + ) + self.uper_min, + ) + return cast(int, dec.read_length_determinant()) def is_empty(self, pkt, # type: ASN1_Packet @@ -792,9 +827,10 @@ def is_empty(self, def _extract_packet_from_decoder(self, dec, pkt): # type: (UPER_Decoder, ASN1_Packet) -> Tuple[Any, bytes] if self.holds_packets: - raise UPER_Decoding_Error( - "UPER SEQUENCE OF packets is not supported yet" - ) + p = self.cls() + p.add_underlayer(pkt) + p.ASN1_root.dissect_from_decoder(p, dec) + return p, b"" return self.fld.m2i_from_decoder(pkt, dec), b"" def m2i_from_decoder(self, pkt, dec): @@ -815,12 +851,36 @@ def _uper_encode_into(self, enc, pkt, value=None): if value is None: value = getattr(pkt, self.name) if value is None: - enc.append_length_determinant(0) + self._uper_count_enc(enc, 0) return - enc.append_length_determinant(len(value)) + count = len(value) + if self.uper_extensible: + if ( + self.uper_min is not None and self.uper_max is not None and + self.uper_min <= count <= self.uper_max + ): + enc.append_bit(0) + else: + enc.append_bit(1) + enc.append_length_determinant(count) + for item in value: + if self.holds_packets: + item_enc = UPER_Encoder() + cast("ASN1_Packet", item).ASN1_root._uper_encode_into( + item_enc, item, + ) + UPER_append_encoded(enc, item_enc.as_bytes()) + else: + self.fld._uper_encode_into(enc, pkt, item) + return + self._uper_count_enc(enc, count) for item in value: if self.holds_packets: - UPER_append_encoded(enc, bytes(item)) + item_enc = UPER_Encoder() + cast("ASN1_Packet", item).ASN1_root._uper_encode_into( + item_enc, item, + ) + UPER_append_encoded(enc, item_enc.as_bytes()) else: self.fld._uper_encode_into(enc, pkt, item) @@ -850,7 +910,10 @@ def m2i(self, return lst, b"" if pkt.ASN1_codec == ASN1_Codecs.PER: dec = UPER_Decoder(s) - count, _ = UPER_count_dec(b"", dec=dec) + if self.uper_extensible and dec.read_bit(): + count = dec.read_length_determinant() + else: + count = self._uper_count_dec(dec) lst = [] for _ in range(count): c, _ = self._extract_packet_from_decoder(dec, pkt) @@ -1006,7 +1069,12 @@ def set_val(self, pkt, val): def is_empty(self, pkt): # type: (ASN1_Packet) -> bool - return self._field.is_empty(pkt) + val = getattr(pkt, self._field.name, None) + if val is None: + return True + if getattr(self._field, "islist", 0) and val == []: + return True + return False def _uper_encode_into(self, enc, pkt, value=None): # type: (UPER_Encoder, ASN1_Packet, Optional[Any]) -> None @@ -1044,6 +1112,7 @@ def __init__(self, name, default, *args, **kwargs): if "implicit_tag" in kwargs: err_msg = "ASN1F_CHOICE has been called with an implicit_tag" raise ASN1_Error(err_msg) + self.uper_extensible = kwargs.pop("uper_extensible", False) self.implicit_tag = None for kwarg in ["context", "explicit_tag"]: setattr(self, kwarg, kwargs.get(kwarg)) @@ -1055,6 +1124,7 @@ def __init__(self, name, default, *args, **kwargs): self.current_choice = None self.choices = {} # type: Dict[int, _CHOICE_T] self.choice_order = [] # type: List[int] + self.choice_list = [] # type: List[_CHOICE_T] self.pktchoices = {} for p in args: if hasattr(p, "ASN1_root"): @@ -1065,22 +1135,27 @@ def __init__(self, name, default, *args, **kwargs): for k in root.choice_order: self.choices[k] = root.choices[k] self.choice_order.append(k) + self.choice_list.append(root.choices[k]) else: tag = p.ASN1_root.network_tag self.choices[tag] = p self.choice_order.append(tag) + self.choice_list.append(p) elif hasattr(p, "ASN1_tag"): if isinstance(p, type): # should be ASN1F_field class tag = int(p.ASN1_tag) self.choices[tag] = p self.choice_order.append(tag) + self.choice_list.append(p) else: # should be ASN1F_field instance tag = p.network_tag self.choices[tag] = p self.choice_order.append(tag) - self.pktchoices[hash(p.cls)] = (p.implicit_tag, p.explicit_tag) # noqa: E501 + self.choice_list.append(p) + if hasattr(p, "cls"): + self.pktchoices[hash(p.cls)] = (p.implicit_tag, p.explicit_tag) # noqa: E501 else: raise ASN1_Error("ASN1F_CHOICE: no tag found for one field") self._tag_to_index = { @@ -1153,42 +1228,59 @@ def m2i(self, pkt, s): def _choice_tag_for(self, x): # type: (Any) -> Optional[int] - for tag, choice in self.choices.items(): - if hasattr(choice, "ASN1_root"): - if isinstance(x, cast("Type[ASN1_Packet]", choice)): - return tag + for index, choice in enumerate(self.choice_list): + if isinstance(choice, type) and hasattr(choice, "ASN1_root"): + if isinstance(x, choice): + return self.choice_order[index] elif isinstance(choice, type) and hasattr(choice, "ASN1_tag"): if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: - return tag + return self.choice_order[index] elif hasattr(choice, "ASN1_tag"): if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: - return tag + return self.choice_order[index] return None def _choice_index_for(self, x): # type: (Any) -> Optional[int] - tag = self._choice_tag_for(x) - if tag is None: - return None - return self._tag_to_index.get(tag) + for index, choice in enumerate(self.choice_list): + if isinstance(choice, type) and hasattr(choice, "ASN1_root"): + if isinstance(x, choice): + return index + elif isinstance(choice, type) and hasattr(choice, "ASN1_tag"): + if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: + return index + elif hasattr(choice, "ASN1_tag"): + if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: + return index + return None def _choice_for_index(self, index): # type: (int) -> _CHOICE_T - return self.choices[self.choice_order[index]] + return self.choice_list[index] def m2i_from_decoder(self, pkt, dec): # type: (ASN1_Packet, UPER_Decoder) -> ASN1_Object[Any] - index, _ = UPER_choice_index_dec(b"", len(self.choice_order), dec=dec) + if self.uper_extensible: + if dec.read_bit(): + raise UPER_Decoding_Error( + "ASN1F_CHOICE: extension additions are not supported" + ) + if len(self.choice_order) > 1: + index, _ = UPER_choice_index_dec(b"", len(self.choice_order), dec=dec) + else: + index = 0 if index >= len(self.choice_order): raise ASN1_Error( "ASN1F_CHOICE: unexpected index %s in '%s'" % (index, self.name) ) choice = self._choice_for_index(index) - if hasattr(choice, "ASN1_root"): - raise ASN1_Error( - "ASN1F_CHOICE: UPER packet choices are not supported yet" - ) + if isinstance(choice, type) and hasattr(choice, "ASN1_root"): + pkt_cls = cast("Type[ASN1_Packet]", choice) + p = pkt_cls() + p.add_underlayer(pkt) + p.ASN1_root.dissect_from_decoder(p, dec) + return cast(ASN1_Object[Any], p) if isinstance(choice, type): return cast( ASN1_Object[Any], @@ -1206,7 +1298,10 @@ def _uper_encode_into(self, enc, pkt, value=None): "ASN1F_CHOICE: cannot encode unknown alternative in '%s'" % self.name ) - UPER_choice_index_enc(index, len(self.choice_order), enc=enc) + if self.uper_extensible: + enc.append_bit(0) + if len(self.choice_order) > 1: + UPER_choice_index_enc(index, len(self.choice_order), enc=enc) choice = self._choice_for_index(index) if hasattr(choice, "ASN1_root"): cast("ASN1_Packet", value).ASN1_root._uper_encode_into(enc, value) @@ -1286,12 +1381,37 @@ def __init__(self, self.network_tag = 16 | 0x20 # 16 + CONSTRUCTED self.default = default + def _resolve_cls(self, pkt): + # type: (ASN1_Packet) -> Type[ASN1_Packet] + if self.next_cls_cb: + return self.next_cls_cb(pkt) or self.cls + return self.cls + + def m2i_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, UPER_Decoder) -> Optional[ASN1_Packet] + cls = self._resolve_cls(pkt) + p = cls() + p.add_underlayer(pkt) + p.ASN1_root.dissect_from_decoder(p, dec) + return p + + def dissect_from_decoder(self, pkt, dec): + # type: (ASN1_Packet, UPER_Decoder) -> None + self.set_val(pkt, self.m2i_from_decoder(pkt, dec)) + + def _uper_encode_into(self, enc, pkt, value=None): + # type: (UPER_Encoder, ASN1_Packet, Any) -> None + if value is None: + value = getattr(pkt, self.name) + if value is None: + return + if isinstance(value, ASN1_Object): + value = value.val + cast("ASN1_Packet", value).ASN1_root._uper_encode_into(enc, value) + def m2i(self, pkt, s): # type: (ASN1_Packet, bytes) -> Tuple[Any, bytes] - if self.next_cls_cb: - cls = self.next_cls_cb(pkt) or self.cls - else: - cls = self.cls + cls = self._resolve_cls(pkt) if not hasattr(cls, "ASN1_root"): # A normal Packet (!= ASN1) return self.extract_packet(cls, s, _underlayer=pkt) @@ -1316,6 +1436,10 @@ def i2m(self, # type: (...) -> bytes if x is None: s = b"" + elif pkt.ASN1_codec == ASN1_Codecs.PER: + enc = UPER_Encoder() + self._uper_encode_into(enc, pkt, x) + s = enc.as_bytes() elif isinstance(x, bytes): s = x elif isinstance(x, ASN1_Object): @@ -1328,6 +1452,8 @@ def i2m(self, if not hasattr(x, "ASN1_root"): # A normal Packet (!= ASN1) return s + if pkt.ASN1_codec == ASN1_Codecs.PER: + return s return BER_tagging_enc(s, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag) @@ -1403,6 +1529,8 @@ def __init__(self, context=None, # type: Optional[Any] implicit_tag=None, # type: Optional[int] explicit_tag=None, # type: Optional[Any] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] ): # type: (...) -> None self.mapping = mapping @@ -1411,7 +1539,9 @@ def __init__(self, default_readable=False, context=context, implicit_tag=implicit_tag, - explicit_tag=explicit_tag + explicit_tag=explicit_tag, + uper_min=uper_min, + uper_max=uper_max, ) def any2i(self, pkt, x): From 53e23c28bfcea347ff41b5331003977461f8b5a4 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 3 Jul 2026 08:16:21 +0200 Subject: [PATCH 07/15] more tests AI-Assisted: yes (Cursor AI) --- scapy/asn1/uper.py | 46 +++- scapy/asn1fields.py | 92 ++++++-- test/scapy/layers/asn1_build_tests.py | 183 ++++++++++++++++ test/scapy/layers/asn1_dissect_tests.py | 278 ++++++++++++++++++++++++ test/scapy/layers/ber_packets.py | 150 +++++++++++++ 5 files changed, 727 insertions(+), 22 deletions(-) create mode 100644 test/scapy/layers/asn1_build_tests.py create mode 100644 test/scapy/layers/asn1_dissect_tests.py create mode 100644 test/scapy/layers/ber_packets.py diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index f6ae7de82e0..6fe1c5cc816 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -119,6 +119,19 @@ def __init__(self): self.chunks_number_of_bits = 0 self.chunks = [] # type: List[List[int]] + def number_of_bytes(self): + # type: () -> int + return (self.chunks_number_of_bits + self.number_of_bits + 7) // 8 + + def align_always(self): + # type: () -> None + width = 8 * self.number_of_bytes() + width -= self.chunks_number_of_bits + width -= self.number_of_bits + if width: + self.number_of_bits += width + self.value <<= width + def append_bit(self, bit): # type: (int) -> None self.number_of_bits += 1 @@ -366,6 +379,15 @@ def read_non_negative_binary_integer(self, number_of_bits): self.number_of_bits -= number_of_bits return value + def align_always(self): + # type: () -> None + consumed = self.total_number_of_bits - self.number_of_bits + width = (8 - (consumed % 8)) % 8 + if width: + if width > self.number_of_bits: + raise UPER_Decoding_Error("UPER_Decoder: out of data") + self.number_of_bits -= width + def read_length_determinant(self): # type: () -> int value = self.read_non_negative_binary_integer(8) @@ -415,6 +437,13 @@ def UPER_constrained_int_dec(s, minimum, maximum): return value + minimum, b"" +def UPER_constrained_int_dec_from_decoder(dec, minimum, maximum): + # type: (UPER_Decoder, int, int) -> int + size = maximum - minimum + value = dec.read_non_negative_binary_integer(UPER_bits_for_range(size)) + return value + minimum + + def UPER_unconstrained_int_enc(value, enc=None): # type: (int, Optional[UPER_Encoder]) -> bytes standalone = enc is None @@ -667,9 +696,17 @@ def encode_into(cls, uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] oer_unsigned=False, # type: bool + uper_extensible=False, # type: bool ): # type: (...) -> None minimum, maximum = _uper_int_range(size_len, uper_min, uper_max, oer_unsigned) + if uper_extensible and minimum is not None and maximum is not None: + if minimum <= i <= maximum: + enc.append_bit(0) + else: + enc.append_bit(1) + UPER_unconstrained_int_enc(i, enc=enc) + return if minimum is not None and maximum is not None: UPER_constrained_int_enc(i, minimum, maximum, enc=enc) else: @@ -682,13 +719,16 @@ def dec_from_decoder(cls, uper_min=None, # type: Optional[int] uper_max=None, # type: Optional[int] oer_unsigned=False, # type: bool + uper_extensible=False, # type: bool ): # type: (...) -> ASN1_Object[int] minimum, maximum = _uper_int_range(size_len, uper_min, uper_max, oer_unsigned) + if uper_extensible and minimum is not None and maximum is not None: + if dec.read_bit(): + value = dec.read_unconstrained_whole_number() + return cls.asn1_object(value) if minimum is not None and maximum is not None: - value = dec.read_non_negative_binary_integer( - UPER_bits_for_range(maximum - minimum) - ) + minimum + value = UPER_constrained_int_dec_from_decoder(dec, minimum, maximum) else: value = dec.read_unconstrained_whole_number() return cls.asn1_object(value) diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index a30f9d7f8d9..0cabb361db4 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -333,6 +333,11 @@ def _uper_codec_kwargs(self, size_len=None): "uper_min": self.uper_min, "uper_max": self.uper_max, } # type: Dict[str, Any] + if ( + getattr(self, "uper_extensible", False) and + self.ASN1_tag == ASN1_Class_UNIVERSAL.INTEGER + ): + kwargs["uper_extensible"] = True if self.uper_enum_values is not None: kwargs["uper_enum_values"] = self.uper_enum_values return kwargs @@ -396,6 +401,29 @@ def randval(self): class ASN1F_INTEGER(ASN1F_field[int, ASN1_INTEGER]): ASN1_tag = ASN1_Class_UNIVERSAL.INTEGER + def __init__(self, + name, # type: str + default, # type: Optional[Union[int, ASN1_INTEGER]] + context=None, # type: Optional[Type[ASN1_Class]] + implicit_tag=None, # type: Optional[int] + explicit_tag=None, # type: Optional[int] + flexible_tag=False, # type: Optional[bool] + size_len=None, # type: Optional[int] + oer_unsigned=False, # type: Optional[bool] + uper_min=None, # type: Optional[int] + uper_max=None, # type: Optional[int] + uper_extensible=False, # type: bool + ): + # type: (...) -> None + super(ASN1F_INTEGER, self).__init__( + name, cast(Optional[ASN1_INTEGER], default), context=context, + implicit_tag=implicit_tag, explicit_tag=explicit_tag, + flexible_tag=flexible_tag, size_len=size_len, + oer_unsigned=oer_unsigned, uper_min=uper_min, + uper_max=uper_max, + ) + self.uper_extensible = uper_extensible + def randval(self): # type: () -> RandNum return RandNum(-2**64, 2**64 - 1) @@ -581,16 +609,17 @@ class ASN1F_SEQUENCE(ASN1F_field[List[Any], List[Any]]): def __init__(self, *seq, **kwargs): # type: (*Any, **Any) -> None - self.uper_extensible = kwargs.pop("uper_extensible", False) + uper_extensible = kwargs.pop("uper_extensible", False) name = "dummy_seq_name" default = [field.default for field in seq] super(ASN1F_SEQUENCE, self).__init__( name, default, **kwargs ) + self.uper_extensible = uper_extensible self.seq = seq self.islist = len(seq) > 1 self._optionals = tuple( - f for f in seq if isinstance(f, ASN1F_optional) + f for f in seq if isinstance(f, (ASN1F_optional, ASN1F_DEFAULT)) ) def __repr__(self): @@ -701,9 +730,9 @@ def _uper_dissect_from_decoder(self, pkt, dec): presence = [dec.read_bit() for _ in self._optionals] opt_idx = 0 for obj in self.seq: - if isinstance(obj, ASN1F_optional): + if isinstance(obj, (ASN1F_optional, ASN1F_DEFAULT)): if not presence[opt_idx]: - obj.set_val(pkt, None) + obj.set_absent(pkt) opt_idx += 1 continue opt_idx += 1 @@ -738,7 +767,7 @@ def _uper_encode_into(self, enc, pkt, value=None): for opt in self._optionals: enc.append_bit(0 if opt.is_empty(pkt) else 1) for obj in self.seq: - if isinstance(obj, ASN1F_optional) and obj.is_empty(pkt): + if isinstance(obj, (ASN1F_optional, ASN1F_DEFAULT)) and obj.is_empty(pkt): continue obj._uper_encode_into(enc, pkt) @@ -835,7 +864,10 @@ def _extract_packet_from_decoder(self, dec, pkt): def m2i_from_decoder(self, pkt, dec): # type: (ASN1_Packet, UPER_Decoder) -> List[Any] - count, _ = UPER_count_dec(b"", dec=dec) + if self.uper_extensible and dec.read_bit(): + count = dec.read_length_determinant() + else: + count = self._uper_count_dec(dec) lst = [] for _ in range(count): item, _ = self._extract_packet_from_decoder(dec, pkt) @@ -865,22 +897,18 @@ def _uper_encode_into(self, enc, pkt, value=None): enc.append_length_determinant(count) for item in value: if self.holds_packets: - item_enc = UPER_Encoder() cast("ASN1_Packet", item).ASN1_root._uper_encode_into( - item_enc, item, + enc, item, ) - UPER_append_encoded(enc, item_enc.as_bytes()) else: self.fld._uper_encode_into(enc, pkt, item) return self._uper_count_enc(enc, count) for item in value: if self.holds_packets: - item_enc = UPER_Encoder() cast("ASN1_Packet", item).ASN1_root._uper_encode_into( - item_enc, item, + enc, item, ) - UPER_append_encoded(enc, item_enc.as_bytes()) else: self.fld._uper_encode_into(enc, pkt, item) @@ -960,12 +988,7 @@ def build(self, pkt): else: if pkt.ASN1_codec == ASN1_Codecs.PER: enc = UPER_Encoder() - enc.append_length_determinant(len(val)) - for item in val: - if self.holds_packets: - UPER_append_encoded(enc, bytes(item)) - else: - self.fld._uper_encode_into(enc, pkt, item) + self._uper_encode_into(enc, pkt, val) s = enc.as_bytes() elif self.holds_packets: s = b"".join(bytes(i) for i in val) @@ -1067,6 +1090,10 @@ def set_val(self, pkt, val): # type: (ASN1_Packet, Any) -> None self._field.set_val(pkt, val) + def set_absent(self, pkt): + # type: (ASN1_Packet) -> None + self.set_val(pkt, None) + def is_empty(self, pkt): # type: (ASN1_Packet) -> bool val = getattr(pkt, self._field.name, None) @@ -1081,6 +1108,32 @@ def _uper_encode_into(self, enc, pkt, value=None): self._field._uper_encode_into(enc, pkt, value) +class ASN1F_DEFAULT(ASN1F_optional): + """ + ASN.1 field with a DEFAULT value (PER presence bit). + """ + def __init__(self, field, default): + # type: (ASN1F_field[Any, Any], Any) -> None + super(ASN1F_DEFAULT, self).__init__(field) + self._default = default + + def is_empty(self, pkt): + # type: (ASN1_Packet) -> bool + val = getattr(pkt, self._field.name, None) + if val is None: + return True + if isinstance(val, ASN1_Object): + val = val.val + default = self._default + if isinstance(default, ASN1_Object): + default = default.val + return bool(val == default) + + def set_absent(self, pkt): + # type: (ASN1_Packet) -> None + self.set_val(pkt, self._default) + + class ASN1F_omit(ASN1F_field[None, None]): """ ASN.1 field that is not specified. This is simply omitted on the network. @@ -1112,7 +1165,7 @@ def __init__(self, name, default, *args, **kwargs): if "implicit_tag" in kwargs: err_msg = "ASN1F_CHOICE has been called with an implicit_tag" raise ASN1_Error(err_msg) - self.uper_extensible = kwargs.pop("uper_extensible", False) + uper_extensible = kwargs.pop("uper_extensible", False) self.implicit_tag = None for kwarg in ["context", "explicit_tag"]: setattr(self, kwarg, kwargs.get(kwarg)) @@ -1120,6 +1173,7 @@ def __init__(self, name, default, *args, **kwargs): name, None, context=self.context, explicit_tag=self.explicit_tag ) + self.uper_extensible = uper_extensible self.default = default self.current_choice = None self.choices = {} # type: Dict[int, _CHOICE_T] diff --git a/test/scapy/layers/asn1_build_tests.py b/test/scapy/layers/asn1_build_tests.py new file mode 100644 index 00000000000..e6c7ad1921c --- /dev/null +++ b/test/scapy/layers/asn1_build_tests.py @@ -0,0 +1,183 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +Cross-codec ASN.1 packet build and round-trip tests (BER, OER, PER). +""" + +from scapy.asn1.asn1 import ASN1_Codecs, ASN1_INTEGER, ASN1_STRING +from scapy.asn1fields import ( + ASN1F_BOOLEAN, + ASN1F_CHOICE, + ASN1F_DEFAULT, + ASN1F_INTEGER, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, + ASN1F_STRING, + ASN1F_optional, +) +from scapy.asn1packet import ASN1_Packet +from scapy.packet import raw + +from typing import Any + +from test.scapy.layers.ber_packets import BERRecord +from test.scapy.layers.oer_packets import OERRecord +from test.scapy.layers.uper_packets import UPERRecord + + +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) + + +def _record_kwargs(): + # type: () -> dict + return dict( + id=42, + flag=True, + label=b"hi", + extra=7, + values=[1, 2, 3], + ) + + +def check_ber_record_build_roundtrip(): + # type: () -> None + pkt = BERRecord(**_record_kwargs()) + assert len(raw(pkt)) > 0 + decoded = _roundtrip(BERRecord, pkt) + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"hi" + assert decoded.extra.val == 7 + assert [x.val for x in decoded.values] == [1, 2, 3] + + +def check_oer_record_build_roundtrip(): + # type: () -> None + pkt = OERRecord(**_record_kwargs()) + assert len(raw(pkt)) > 0 + decoded = _roundtrip(OERRecord, pkt) + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"hi" + assert decoded.extra.val == 7 + assert [x.val for x in decoded.values] == [1, 2, 3] + + +def check_per_record_build_roundtrip(): + # type: () -> None + pkt = UPERRecord(**_record_kwargs()) + assert len(raw(pkt)) > 0 + decoded = _roundtrip(UPERRecord, pkt) + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"hi" + assert decoded.extra.val == 7 + assert [x.val for x in decoded.values] == [1, 2, 3] + + +def _asn1_int(val): + # type: (Any) -> int + return val.val if hasattr(val, "val") else val + + +def check_per_default_field_build(): + # type: () -> None + class UPERDefaultRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_DEFAULT( + ASN1F_INTEGER( + "count", 600, + uper_min=0, uper_max=86401, oer_unsigned=True, + ), + 600, + ), + ) + + absent = UPERDefaultRecord(id=1) + assert raw(absent) == b"\x00\x80" + decoded = _roundtrip(UPERDefaultRecord, absent) + assert decoded.id.val == 1 + assert _asn1_int(decoded.count) == 600 + + present = UPERDefaultRecord(id=1, count=86400) + assert raw(present) == bytes.fromhex("80d46000") + decoded = _roundtrip(UPERDefaultRecord, present) + assert decoded.id.val == 1 + assert _asn1_int(decoded.count) == 86400 + + +def check_per_extensible_integer_build(): + # type: () -> None + class UPERExtInt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER( + "n", 0, + uper_min=1, uper_max=65535, + uper_extensible=True, oer_unsigned=True, + ), + ) + + in_range = UPERExtInt(n=42) + assert raw(in_range) == bytes.fromhex("001480") + decoded = _roundtrip(UPERExtInt, in_range) + assert decoded.n.val == 42 + + out_of_range = UPERExtInt(n=1706733817) + assert raw(out_of_range) == bytes.fromhex("8232dd587c80") + decoded = _roundtrip(UPERExtInt, out_of_range) + assert decoded.n.val == 1706733817 + + +def check_per_constrained_sequence_of_build(): + # type: () -> None + class UPERConstrainedSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "items", [], + ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7), + uper_min=1, uper_max=3, + ) + + pkt = UPERConstrainedSeqOf(items=[1, 2]) + assert raw(pkt) == bytes.fromhex("4a") + decoded = _roundtrip(UPERConstrainedSeqOf, pkt) + assert [x.val for x in decoded.items] == [1, 2] + + +def check_ber_oer_per_choice_build(): + # type: () -> None + class BERChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + class OERChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + class PERChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + for cls in (BERChoice, OERChoice, PERChoice): + as_int = cls(c=ASN1_INTEGER(99)) + assert len(raw(as_int)) > 0 + decoded = _roundtrip(cls, as_int) + assert decoded.c.val == 99 + + as_str = cls(c=ASN1_STRING(b"AB")) + assert len(raw(as_str)) > 0 + decoded = _roundtrip(cls, as_str) + assert decoded.c.val == b"AB" diff --git a/test/scapy/layers/asn1_dissect_tests.py b/test/scapy/layers/asn1_dissect_tests.py new file mode 100644 index 00000000000..9960e3d70e6 --- /dev/null +++ b/test/scapy/layers/asn1_dissect_tests.py @@ -0,0 +1,278 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +ASN.1 packet dissection tests from fixed byte vectors (BER, OER, PER). +""" + +from typing import Any, Type + +from scapy.asn1.asn1 import ASN1_Codecs +from scapy.asn1fields import ( + ASN1F_DEFAULT, + ASN1F_INTEGER, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, +) +from scapy.asn1packet import ASN1_Packet + +from test.scapy.layers.ber_packets import ( + BERChoiceField, + BERFixedFields, + BEROptionalField, + BERRecord, + BERSequenceOfIntegers, + BERTaggedInteger, +) +from test.scapy.layers.oer_packets import ( + OERChoiceField, + OERFixedFields, + OEROptionalField, + OERRecord, + OERSequenceOfIntegers, + OERTaggedInteger, +) +from test.scapy.layers.uper_packets import ( + UPERChoiceField, + UPERFixedFields, + UPEROptionalField, + UPERRecord, + UPERSequenceOfIntegers, +) + + +def _asn1_int(val): + # type: (Any) -> int + return val.val if hasattr(val, "val") else val + + +def _assert_record(decoded): + # type: (ASN1_Packet) -> None + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"hi" + assert decoded.extra.val == 7 + assert [x.val for x in decoded.values] == [1, 2, 3] + + +def _assert_record_empty(decoded): + # type: (ASN1_Packet) -> None + assert decoded.id.val == 1 + assert decoded.flag.val == 0 + assert decoded.label.val == b"" + assert decoded.extra is None + assert [x.val for x in decoded.values] == [] + + +def _dissect(cls, data_hex): + # type: (Type[ASN1_Packet], str) -> ASN1_Packet + return cls(bytes.fromhex(data_hex)) + + +def check_ber_field_dissect(): + # type: () -> None + tagged = _dissect(BERTaggedInteger, "a103020105") + assert tagged.n.val == 5 + + fixed = _dissect(BERFixedFields, "300d02810200c80483000003414243") + assert fixed.n.val == 200 + assert fixed.s.val == b"ABC" + + present = _dissect(BEROptionalField, "3008020101a003020107") + assert present.id.val == 1 + assert present.extra.val == 7 + + absent = _dissect(BEROptionalField, "3003020101") + assert absent.id.val == 1 + assert absent.extra is None + + seqof = _dissect(BERSequenceOfIntegers, "3009020101020102020103") + assert [x.val for x in seqof.values] == [1, 2, 3] + + as_int = _dissect(BERChoiceField, "020163") + assert as_int.c.val == 99 + + as_str = _dissect(BERChoiceField, "040178") + assert as_str.c.val == b"x" + + +def check_ber_record_dissect(): + # type: () -> None + decoded = _dissect( + BERRecord, + "301a02012a01010104026869" + "a003020107" + "3009020101020102020103", + ) + _assert_record(decoded) + + empty = _dissect(BERRecord, "300a02010101010004003000") + _assert_record_empty(empty) + + +def check_oer_field_dissect(): + # type: () -> None + tagged = _dissect(OERTaggedInteger, "a10105") + assert tagged.n.val == 5 + + fixed = _dissect(OERFixedFields, "c8414243") + assert fixed.n.val == 200 + assert fixed.s.val == b"ABC" + + present = _dissect(OEROptionalField, "0101a00107") + assert present.id.val == 1 + assert present.extra.val == 7 + + absent = _dissect(OEROptionalField, "0101") + assert absent.id.val == 1 + assert absent.extra is None + + seqof = _dissect(OERSequenceOfIntegers, "0103010101020103") + assert [x.val for x in seqof.values] == [1, 2, 3] + + as_int = _dissect(OERChoiceField, "020163") + assert as_int.c.val == 99 + + as_str = _dissect(OERChoiceField, "040178") + assert as_str.c.val == b"x" + + +def check_oer_record_dissect(): + # type: () -> None + decoded = _dissect( + OERRecord, + "012aff026869a00107" + "0103010101020103", + ) + _assert_record(decoded) + + empty = _dissect(OERRecord, "010100000100") + _assert_record_empty(empty) + + +def check_per_field_dissect(): + # type: () -> None + fixed = _dissect(UPERFixedFields, "c8414243") + assert fixed.n.val == 200 + assert fixed.s.val == b"ABC" + + present = _dissect(UPEROptionalField, "80954041c0") + assert present.id.val == 42 + assert present.flag.val == 1 + assert present.extra.val == 7 + + absent = _dissect(UPEROptionalField, "009540") + assert absent.id.val == 42 + assert absent.flag.val == 1 + assert absent.extra is None + + seqof = _dissect(UPERSequenceOfIntegers, "03010101020103") + assert [x.val for x in seqof.values] == [1, 2, 3] + + empty_seqof = _dissect(UPERSequenceOfIntegers, "00") + assert [x.val for x in empty_seqof.values] == [] + + as_int = _dissect(UPERChoiceField, "00b180") + assert as_int.c.val == 99 + + as_str = _dissect(UPERChoiceField, "8120a100") + assert as_str.c.val == b"AB" + + +def check_per_record_dissect(): + # type: () -> None + decoded = _dissect( + UPERRecord, + "8095409a1a4041c0c04040408040c0", + ) + _assert_record(decoded) + + partial = _dissect(UPERRecord, "0095409050808040404080") + assert partial.id.val == 42 + assert partial.flag.val == 1 + assert partial.label.val == b"AB" + assert partial.extra is None + assert [x.val for x in partial.values] == [1, 2] + + empty = _dissect(UPERRecord, "0080800000") + _assert_record_empty(empty) + + +def check_per_default_field_dissect(): + # type: () -> None + class UPERDefaultRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_DEFAULT( + ASN1F_INTEGER( + "count", 600, + uper_min=0, uper_max=86401, oer_unsigned=True, + ), + 600, + ), + ) + + absent = _dissect(UPERDefaultRecord, "0080") + assert absent.id.val == 1 + assert _asn1_int(absent.count) == 600 + + present = _dissect(UPERDefaultRecord, "80d46000") + assert present.id.val == 1 + assert _asn1_int(present.count) == 86400 + + +def check_per_extensible_integer_dissect(): + # type: () -> None + class UPERExtInt(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER( + "n", 0, + uper_min=1, uper_max=65535, + uper_extensible=True, oer_unsigned=True, + ), + ) + + in_range = _dissect(UPERExtInt, "001480") + assert in_range.n.val == 42 + + out_of_range = _dissect(UPERExtInt, "8232dd587c80") + assert out_of_range.n.val == 1706733817 + + +def check_per_constrained_sequence_of_dissect(): + # type: () -> None + class UPERConstrainedSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "items", [], + ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7), + uper_min=1, uper_max=3, + ) + + decoded = _dissect(UPERConstrainedSeqOf, "4a") + assert [x.val for x in decoded.items] == [1, 2] + + +def check_ber_oer_per_record_dissect(): + # type: () -> None + for cls, data_hex in [ + ( + BERRecord, + "301a02012a01010104026869" + "a003020107" + "3009020101020102020103", + ), + ( + OERRecord, + "012aff026869a00107" + "0103010101020103", + ), + ( + UPERRecord, + "8095409a1a4041c0c04040408040c0", + ), + ]: + _assert_record(_dissect(cls, data_hex)) diff --git a/test/scapy/layers/ber_packets.py b/test/scapy/layers/ber_packets.py new file mode 100644 index 00000000000..aad50722c11 --- /dev/null +++ b/test/scapy/layers/ber_packets.py @@ -0,0 +1,150 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +BER ASN1_Packet and ASN1F_field build tests. +""" + +from scapy.asn1.asn1 import ASN1_Codecs, ASN1_INTEGER, ASN1_STRING +from scapy.asn1fields import ( + ASN1F_BOOLEAN, + ASN1F_CHOICE, + ASN1F_INTEGER, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, + ASN1F_STRING, + ASN1F_optional, +) +from scapy.asn1packet import ASN1_Packet +from scapy.packet import raw + + +class BERTaggedInteger(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_INTEGER("n", 0, explicit_tag=0xA1) + + +class BERFixedFields(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("n", 0, size_len=1, oer_unsigned=True), + ASN1F_STRING("s", "", size_len=3), + ) + + +class BEROptionalField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), + ) + + +class BERSequenceOfIntegers(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + + +class BERChoiceField(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + +class BERRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_BOOLEAN("flag", False), + ASN1F_STRING("label", ""), + ASN1F_optional(ASN1F_INTEGER("extra", 0, explicit_tag=0xA0)), + ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER), + ) + + +def _roundtrip(cls, pkt): + # type: (type, ASN1_Packet) -> ASN1_Packet + return cls(raw(pkt)) + + +def check_ber_field_explicit_tag(): + # type: () -> None + pkt = BERTaggedInteger(n=5) + assert raw(pkt) == b"\xa1\x03\x02\x01\x05" + decoded = _roundtrip(BERTaggedInteger, pkt) + assert decoded.n.val == 5 + + +def check_ber_field_fixed_size(): + # type: () -> None + pkt = BERFixedFields(n=200, s=b"ABC") + assert raw(pkt) == bytes.fromhex("300d02810200c80483000003414243") + decoded = _roundtrip(BERFixedFields, pkt) + assert decoded.n.val == 200 + assert decoded.s.val == b"ABC" + + +def check_ber_field_optional(): + # type: () -> None + present = BEROptionalField(id=1, extra=7) + assert raw(present) == bytes.fromhex("3008020101a003020107") + decoded = _roundtrip(BEROptionalField, present) + assert decoded.id.val == 1 + assert decoded.extra.val == 7 + + absent = BEROptionalField(id=1, extra=None) + assert raw(absent) == bytes.fromhex("3003020101") + decoded = _roundtrip(BEROptionalField, absent) + assert decoded.id.val == 1 + assert decoded.extra is None + + +def check_ber_field_sequence_of(): + # type: () -> None + pkt = BERSequenceOfIntegers(values=[1, 2, 3]) + assert raw(pkt) == b"\x30\x09\x02\x01\x01\x02\x01\x02\x02\x01\x03" + decoded = _roundtrip(BERSequenceOfIntegers, pkt) + assert [x.val for x in decoded.values] == [1, 2, 3] + + +def check_ber_field_choice(): + # type: () -> None + as_int = BERChoiceField(c=ASN1_INTEGER(99)) + assert raw(as_int) == b"\x02\x01c" + decoded = _roundtrip(BERChoiceField, as_int) + assert decoded.c.val == 99 + + as_str = BERChoiceField(c=ASN1_STRING("x")) + assert raw(as_str) == b"\x04\x01x" + decoded = _roundtrip(BERChoiceField, as_str) + assert decoded.c.val == b"x" + + +def check_ber_packet_record(): + # type: () -> None + pkt = BERRecord( + id=42, flag=True, label="hi", extra=7, values=[1, 2, 3], + ) + expected = bytes.fromhex( + "301a02012a01010104026869" + "a003020107" + "3009020101020102020103" + ) + assert raw(pkt) == expected + decoded = _roundtrip(BERRecord, pkt) + assert decoded.id.val == 42 + assert decoded.flag.val == 1 + assert decoded.label.val == b"hi" + assert decoded.extra.val == 7 + assert [x.val for x in decoded.values] == [1, 2, 3] + + empty = BERRecord(id=1, flag=False, label="", extra=None, values=[]) + assert raw(empty) == bytes.fromhex("300a02010101010004003000") + decoded = _roundtrip(BERRecord, empty) + assert decoded.id.val == 1 + assert decoded.flag.val == 0 + assert decoded.label.val == b"" + assert decoded.extra is None + assert [x.val for x in decoded.values] == [] From 68477e897b7b8cdadc42e89832732c8ba7918d7d Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 3 Jul 2026 08:20:06 +0200 Subject: [PATCH 08/15] more tests AI-Assisted: yes (Cursor AI) --- test/scapy/layers/asn1.uts | 52 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index 8f94db57923..763d6b78650 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -256,6 +256,58 @@ __import__('test.scapy.layers.oer_packets', fromlist=['check_oer_field_choice']) = OER packet record __import__('test.scapy.layers.oer_packets', fromlist=['check_oer_packet_record']).check_oer_packet_record() ++ ASN.1 BER packets and fields += BER field explicit tag +__import__('test.scapy.layers.ber_packets', fromlist=['check_ber_field_explicit_tag']).check_ber_field_explicit_tag() += BER field fixed size +__import__('test.scapy.layers.ber_packets', fromlist=['check_ber_field_fixed_size']).check_ber_field_fixed_size() += BER field optional +__import__('test.scapy.layers.ber_packets', fromlist=['check_ber_field_optional']).check_ber_field_optional() += BER field sequence of +__import__('test.scapy.layers.ber_packets', fromlist=['check_ber_field_sequence_of']).check_ber_field_sequence_of() += BER field choice +__import__('test.scapy.layers.ber_packets', fromlist=['check_ber_field_choice']).check_ber_field_choice() += BER packet record +__import__('test.scapy.layers.ber_packets', fromlist=['check_ber_packet_record']).check_ber_packet_record() + ++ ASN.1 packet build tests (BER, OER, PER) += BER record build roundtrip +__import__('test.scapy.layers.asn1_build_tests', fromlist=['check_ber_record_build_roundtrip']).check_ber_record_build_roundtrip() += OER record build roundtrip +__import__('test.scapy.layers.asn1_build_tests', fromlist=['check_oer_record_build_roundtrip']).check_oer_record_build_roundtrip() += PER record build roundtrip +__import__('test.scapy.layers.asn1_build_tests', fromlist=['check_per_record_build_roundtrip']).check_per_record_build_roundtrip() += PER default field build +__import__('test.scapy.layers.asn1_build_tests', fromlist=['check_per_default_field_build']).check_per_default_field_build() += PER extensible integer build +__import__('test.scapy.layers.asn1_build_tests', fromlist=['check_per_extensible_integer_build']).check_per_extensible_integer_build() += PER constrained sequence of build +__import__('test.scapy.layers.asn1_build_tests', fromlist=['check_per_constrained_sequence_of_build']).check_per_constrained_sequence_of_build() += BER OER PER choice build +__import__('test.scapy.layers.asn1_build_tests', fromlist=['check_ber_oer_per_choice_build']).check_ber_oer_per_choice_build() + ++ ASN.1 packet dissection tests (BER, OER, PER) += BER field dissect +__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_ber_field_dissect']).check_ber_field_dissect() += BER record dissect +__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_ber_record_dissect']).check_ber_record_dissect() += OER field dissect +__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_oer_field_dissect']).check_oer_field_dissect() += OER record dissect +__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_oer_record_dissect']).check_oer_record_dissect() += PER field dissect +__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_per_field_dissect']).check_per_field_dissect() += PER record dissect +__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_per_record_dissect']).check_per_record_dissect() += PER default field dissect +__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_per_default_field_dissect']).check_per_default_field_dissect() += PER extensible integer dissect +__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_per_extensible_integer_dissect']).check_per_extensible_integer_dissect() += PER constrained sequence of dissect +__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_per_constrained_sequence_of_dissect']).check_per_constrained_sequence_of_dissect() += BER OER PER record dissect +__import__('test.scapy.layers.asn1_dissect_tests', fromlist=['check_ber_oer_per_record_dissect']).check_ber_oer_per_record_dissect() + + ASN.1 UPER codec = UPER boolean true UPERcodec_BOOLEAN.enc(1) == b"\x80" From 1712f4517ec644be0173e0b91d941be6ca57c75a Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 3 Jul 2026 08:35:48 +0200 Subject: [PATCH 09/15] fix flake8 AI-Assisted: yes (Cursor AI) --- scapy/asn1fields.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 0cabb361db4..42874a56f57 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -45,12 +45,10 @@ UPER_Decoding_Error, UPER_Decoder, UPER_Encoder, - UPER_append_encoded, UPER_bits_for_range, UPER_choice_index_dec, UPER_choice_index_enc, UPER_constrained_int_enc, - UPER_count_dec, UPER_has_unexpected_remainder, ) from scapy.base_classes import BasePacket From 9b336abc38a644e4ab9af6ce0a9c7470e7b77891 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 3 Jul 2026 09:03:06 +0200 Subject: [PATCH 10/15] increase coverage AI-Assisted: yes (Cursor AI) --- test/scapy/layers/asn1.uts | 14 + test/scapy/layers/asn1_coverage.py | 550 ++++++++++++++++++++++++++++- 2 files changed, 563 insertions(+), 1 deletion(-) diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index 763d6b78650..ec5e835ff21 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -517,3 +517,17 @@ __import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_encaps __import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_choice_and_special']).check_asn1fields_choice_and_special() = asn1fields optional dissect __import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_optional_dissect']).check_asn1fields_optional_dissect() += asn1fields default and omit +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_default_and_omit']).check_asn1fields_default_and_omit() += asn1fields extensible PER +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_extensible_per']).check_asn1fields_extensible_per() += asn1fields sequence of advanced +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_sequence_of_advanced']).check_asn1fields_sequence_of_advanced() += asn1fields choice advanced +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_choice_advanced']).check_asn1fields_choice_advanced() += asn1fields enum bitstring and flags +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_enum_bitstring_and_flags']).check_asn1fields_enum_bitstring_and_flags() += asn1fields packet and sequence errors +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_packet_and_sequence_errors']).check_asn1fields_packet_and_sequence_errors() += asn1fields more coverage +__import__('test.scapy.layers.asn1_coverage', fromlist=['check_asn1fields_more_coverage']).check_asn1fields_more_coverage() diff --git a/test/scapy/layers/asn1_coverage.py b/test/scapy/layers/asn1_coverage.py index d03de639f0a..ef8168fd604 100644 --- a/test/scapy/layers/asn1_coverage.py +++ b/test/scapy/layers/asn1_coverage.py @@ -17,14 +17,18 @@ def _raises(exc, func): from typing import Any +from unittest import mock from scapy.asn1.asn1 import ( ASN1_BIT_STRING, + ASN1_Class_UNIVERSAL, ASN1_Codecs, + ASN1_Error, ASN1_INTEGER, ASN1_STRING, ASN1_TIME_TICKS, ) +from scapy.asn1.ber import BER_Decoding_Error from scapy.asn1.oer import ( OER_Decoding_Error, OER_Encoding_Error, @@ -45,23 +49,31 @@ def _raises(exc, func): UPERcodec_SET, ) from scapy.asn1fields import ( + ASN1F_BIT_STRING, ASN1F_BIT_STRING_ENCAPS, + ASN1F_BOOLEAN, ASN1F_CHOICE, + ASN1F_DEFAULT, ASN1F_FLAGS, ASN1F_IPADDRESS, ASN1F_INTEGER, + ASN1F_OID, ASN1F_PACKET, ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, ASN1F_SET_OF, ASN1F_STRING, ASN1F_STRING_ENCAPS, ASN1F_STRING_PacketField, ASN1F_TIME_TICKS, + ASN1F_UTC_TIME, + ASN1F_badsequence, ASN1F_enum_INTEGER, + ASN1F_omit, ASN1F_optional, ) from scapy.asn1packet import ASN1_Packet -from scapy.packet import raw +from scapy.packet import Raw, raw class _InnerRecord(ASN1_Packet): @@ -342,3 +354,539 @@ class _BerChoiceRecord(ASN1_Packet): choice_rand = _BerChoiceRecord.ASN1_root.randval() assert choice_rand is not None + + +def check_asn1fields_default_and_omit(): + # type: () -> None + class _DefaultRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_DEFAULT( + ASN1F_INTEGER( + "count", 600, + uper_min=0, uper_max=86401, oer_unsigned=True, + ), + 600, + ), + ) + + absent = _DefaultRecord(id=1) + assert raw(absent) == b"\x00\x80" + decoded = _DefaultRecord(raw(absent)) + assert decoded.id.val == 1 + assert decoded.count == 600 or decoded.count.val == 600 + + present = _DefaultRecord(id=1, count=86400) + decoded = _DefaultRecord(raw(present)) + assert decoded.count.val == 86400 + + class _OmitRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_omit("ignored", None), + ) + + omit_pkt = _OmitRecord(id=7) + assert raw(omit_pkt) == bytes.fromhex("3003020107") + + +def check_asn1fields_extensible_per(): + # type: () -> None + class _ExtSeq(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_optional(ASN1F_INTEGER("extra", 0, uper_min=0, uper_max=7)), + uper_extensible=True, + ) + + pkt = _ExtSeq(id=2, extra=3) + data = raw(pkt) + decoded = _ExtSeq(data) + assert decoded.id.val == 2 + assert decoded.extra.val == 3 + + dec = UPER_Decoder(b"\x80") + _raises( + UPER_Decoding_Error, + lambda: _ExtSeq.ASN1_root.dissect_from_decoder(_ExtSeq(), dec), + ) + + class _ExtChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + uper_extensible=True, + ) + + choice = _ExtChoice(c=ASN1_INTEGER(4)) + assert raw(choice) + dec = UPER_Decoder(b"\x80") + _raises( + UPER_Decoding_Error, + lambda: _ExtChoice.ASN1_root.m2i_from_decoder(_ExtChoice(), dec), + ) + + class _InnerItem(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7) + + class _ExtSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "items", [], _InnerItem, + uper_min=1, uper_max=2, uper_extensible=True, + ) + + in_range = _ExtSeqOf(items=[_InnerItem(n=1)]) + assert raw(in_range) + decoded = _ExtSeqOf(raw(in_range)) + assert decoded.items[0].n.val == 1 + + out_of_range = _ExtSeqOf( + items=[_InnerItem(n=i) for i in range(4)], + ) + assert raw(out_of_range) + decoded = _ExtSeqOf(raw(out_of_range)) + assert len(decoded.items) == 4 + + +def check_asn1fields_sequence_of_advanced(): + # type: () -> None + class _Inner(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("n", 0, uper_min=0, uper_max=7) + + class _SeqOfPackets(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "items", [], _Inner, uper_min=1, uper_max=3, + ) + + pkt = _SeqOfPackets(items=[_Inner(n=1), _Inner(n=2)]) + decoded = _SeqOfPackets(raw(pkt)) + assert [x.n.val for x in decoded.items] == [1, 2] + + class _OerSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + + oer_pkt = _OerSeqOf(values=[1, 2]) + oer_dec = _OerSeqOf(raw(oer_pkt)) + assert [x.val for x in oer_dec.values] == [1, 2] + + class _EmptySeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF("values", [], ASN1F_INTEGER) + + empty = _EmptySeqOf(values=None) + assert raw(empty) == b"\x00" + assert _EmptySeqOf.ASN1_root.i2repr(empty, None) == "[]" + assert _EmptySeqOf.ASN1_root.i2repr( + _EmptySeqOf(values=[ASN1_INTEGER(1)]), + [ASN1_INTEGER(1)], + ).startswith("[") + + _raises(ValueError, lambda: ASN1F_SEQUENCE_OF("bad", [], object())) + + +def check_asn1fields_choice_advanced(): + # type: () -> None + class _InnerChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + class _NestedChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), _InnerChoice, ASN1F_INTEGER, + ) + + nested = _NestedChoice(c=_InnerChoice(c=ASN1_STRING(b"xy"))) + assert len(raw(nested)) > 0 + nested_dec = _NestedChoice(raw(nested)) + assert isinstance(nested_dec.c, (_InnerChoice, ASN1_STRING)) + + class _OerTaggedChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + explicit_tag=0xA1, + ) + + oer_choice = _OerTaggedChoice(c=ASN1_INTEGER(9)) + assert raw(oer_choice) + + class _PacketChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", + ASN1_INTEGER(0), + ASN1F_PACKET("inner", None, _InnerRecord, explicit_tag=0xA2), + ASN1F_INTEGER, + ) + + packet_choice = _PacketChoice( + c=_InnerRecord(mode=ASN1_INTEGER(1)), + ) + packet_dec = _PacketChoice(raw(packet_choice)) + assert packet_dec.c.mode.val == 1 + + class _PerChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + _raises( + ASN1_Error, + lambda: ASN1F_CHOICE( + "c", 0, ASN1F_INTEGER, implicit_tag=0xA0, + ), + ) + _raises( + ASN1_Error, + lambda: _PerChoice.ASN1_root.m2i(_PerChoice(), b""), + ) + _raises( + ASN1_Error, + lambda: _PerChoice.ASN1_root._uper_encode_into( + UPER_Encoder(), _PerChoice(), 42, + ), + ) + + +def check_asn1fields_enum_bitstring_and_flags(): + # type: () -> None + class _NamedEnum(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_enum_INTEGER( + "state", 0, ["off", "on", "auto"], + ) + + named = _NamedEnum(state="on") + built = raw(named) + decoded = _NamedEnum(built) + assert decoded.state.val == 1 + assert "'on'" in _NamedEnum.ASN1_root.i2repr(decoded, decoded.state) + + class _BitRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_BIT_STRING("bits", b"\xaa") + + assert raw(_BitRecord()) + + flags = _FlagsRecord() + flags.f = ASN1_BIT_STRING("101") + assert "read, exec" in _FlagsRecord.ASN1_root.seq[0].i2repr(flags, flags.f) + + class _BadBitEncaps(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_BIT_STRING_ENCAPS("b", None, _InnerRecord) + + _raises( + BER_Decoding_Error, + lambda: _BadBitEncaps.ASN1_root.m2i( + _BadBitEncaps(), + b"\x03\x02\x01\x00", + ), + ) + + +def check_asn1fields_packet_and_sequence_errors(): + # type: () -> None + class _PerInner(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_INTEGER("mode", 0, uper_min=0, uper_max=1) + + class _PacketWrap(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_PACKET("inner", None, _PerInner) + + inner = _PerInner(mode=1) + wrap = _PacketWrap(inner=inner) + decoded = _PacketWrap(raw(wrap)) + assert decoded.inner.mode.val == 1 + + class _DynamicPacket(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_PACKET( + "inner", None, _PerInner, + next_cls_cb=lambda pkt: _PerInner, + ) + + dyn = _DynamicPacket(inner=_PerInner(mode=0)) + assert _DynamicPacket.ASN1_root._resolve_cls(dyn) is _PerInner + + empty_packet = _PacketWrap(inner=None) + assert raw(empty_packet) == b"" + + class _BerSeq(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + ASN1F_INTEGER("extra", 0), + ) + + _raises( + BER_Decoding_Error, + lambda: _BerSeq.ASN1_root.m2i( + _BerSeq(), + bytes.fromhex("300702010102010200ff"), + ), + ) + + class _OerSeq(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, size_len=1, oer_unsigned=True), + ) + + _raises( + OER_Decoding_Error, + lambda: _OerSeq.ASN1_root.m2i(_OerSeq(), b"\x01\xff"), + ) + + class _PerSeq(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ) + + _raises( + UPER_Decoding_Error, + lambda: _PerSeq.ASN1_root.m2i(_PerSeq(), b"\x80\xff"), + ) + + empty_seq = _BerSeq() + _BerSeq.ASN1_root._dissect_sequence_children(empty_seq, b"") + assert empty_seq.id is None + assert empty_seq.extra is None + + class _OptListRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255), + ASN1F_optional( + ASN1F_SEQUENCE_OF("items", [], ASN1F_INTEGER), + ), + ) + + opt_list = _OptListRecord(id=1, items=None) + assert raw(opt_list) + + field = ASN1F_INTEGER("n", 0) + with mock.patch.object( + _InnerRecord, "__init__", side_effect=ASN1F_badsequence, + ): + pkt_obj, remain = field.extract_packet( + _InnerRecord, b"\xab\xcd", _underlayer=None, + ) + assert isinstance(pkt_obj, Raw) + assert pkt_obj.load == b"\xab\xcd" + assert remain == b"\xab\xcd" + + +def check_asn1fields_more_coverage(): + # type: () -> None + _raises( + ASN1_Error, + lambda: ASN1F_INTEGER("x", 0, implicit_tag=1, explicit_tag=2), + ) + + class _IntRecord(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_INTEGER("n", 0) + + field = _IntRecord.ASN1_root + _raises( + ASN1_Error, + lambda: field.i2m(_IntRecord(), ASN1_STRING(b"bad")), + ) + + flex_field = ASN1F_INTEGER("n", 0, flexible_tag=True, explicit_tag=0xA0) + obj, remain = flex_field.m2i(_IntRecord(), bytes.fromhex("a1020101")) + assert obj.tag != ASN1_Class_UNIVERSAL.INTEGER or remain == b"" + + class _FlexSeq(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0), + explicit_tag=0xA1, + flexible_tag=True, + ) + + flex_seq = _FlexSeq(id=1) + assert raw(flex_seq) + decoded = _FlexSeq(raw(flex_seq)) + assert decoded.id.val == 1 + + assert ASN1F_BOOLEAN("b", False).randval() is not None + assert ASN1F_BIT_STRING("b", b"").randval() is not None + assert ASN1F_OID("o", None).randval() is not None + assert ASN1F_UTC_TIME("t", "").randval() is not None + assert " 0 + + empty_inner, remain = packet_field.m2i(_FlexPacket(), b"") + assert empty_inner is None and remain == b"" + + obj_val = packet_field.i2m(_FlexPacket(), _InnerRecord(mode=0)) + assert len(obj_val) > 0 + + flags_field = _FlagsRecord.ASN1_root.seq[0] + assert flags_field.i2repr(_FlagsRecord(), None) == "None" + + class _OerFlexSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_SEQUENCE_OF( + "values", [], ASN1F_INTEGER, + explicit_tag=0xA1, + ) + + _OerFlexSeqOf.ASN1_root.flexible_tag = True + + oer_seq = _OerFlexSeqOf(values=[1]) + data = raw(oer_seq) + decoded = _OerFlexSeqOf(data) + assert decoded.values[0].val == 1 + + class _BerFlexSeqOf(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE_OF( + "values", [], ASN1F_INTEGER, + explicit_tag=0xA1, + ) + + _BerFlexSeqOf.ASN1_root.flexible_tag = True + + ber_seq = _BerFlexSeqOf(values=[2]) + assert raw(ber_seq) + + class _ExtChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + uper_extensible=True, + ) + + dec = UPER_Decoder(b"\x80") + _raises( + UPER_Decoding_Error, + lambda: _ExtChoice.ASN1_root.m2i_from_decoder(_ExtChoice(), dec), + ) + + class _SingleChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE("c", ASN1_INTEGER(0), ASN1F_INTEGER) + + single = _SingleChoice(c=ASN1_INTEGER(3)) + assert raw(single) + + class _FlexChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + flexible_tag=True, + ) + + flex_choice = _FlexChoice(c=ASN1_INTEGER(4)) + assert raw(flex_choice) + + class _OerPktChoice(ASN1_Packet): + ASN1_codec = ASN1_Codecs.OER + ASN1_root = ASN1F_CHOICE( + "c", ASN1_INTEGER(0), ASN1F_INTEGER, ASN1F_STRING, + ) + + oer_pkt_choice = _OerPktChoice(c=ASN1_STRING(b"hi")) + assert raw(oer_pkt_choice) + From b1bbcf61ea50e1ef1e9d0fff41d5165c7a356fe4 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 3 Jul 2026 11:31:24 +0200 Subject: [PATCH 11/15] Code cleanups AI-Assisted: yes (Sonnet 5) --- scapy/asn1/ber.py | 3 +- scapy/asn1/uper.py | 37 +++++++++-------- scapy/asn1fields.py | 97 ++++++++++++++++----------------------------- 3 files changed, 56 insertions(+), 81 deletions(-) diff --git a/scapy/asn1/ber.py b/scapy/asn1/ber.py index 1f9a9e75463..2be68edfef5 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -11,6 +11,7 @@ # Good read: https://luca.ntop.org/Teaching/Appunti/asn1.html +from scapy.config import conf from scapy.error import warning from scapy.compat import chb, orb, bytes_encode from scapy.utils import binrepr, inet_aton, inet_ntoa @@ -257,7 +258,6 @@ def BER_tagging_dec(s, # type: bytes def BER_tagging_enc(s, implicit_tag=None, explicit_tag=None): # type: (bytes, Optional[int], Optional[int]) -> bytes - from scapy.config import conf if len(s) > 0: if implicit_tag is not None: s = BER_id_enc(implicit_tag) + s[1:] @@ -632,7 +632,6 @@ class BERcodec_SEQUENCE(BERcodec_Object[Union[bytes, List[BERcodec_Object[Any]]] @classmethod def enc(cls, _ll, size_len=0): # type: (Union[bytes, List[BERcodec_Object[Any]]], Optional[int]) -> bytes - from scapy.config import conf if isinstance(_ll, bytes): ll = _ll else: diff --git a/scapy/asn1/uper.py b/scapy/asn1/uper.py index 6fe1c5cc816..3d6c1c780f6 100644 --- a/scapy/asn1/uper.py +++ b/scapy/asn1/uper.py @@ -668,6 +668,13 @@ def enc(cls, s, size_len=0, uper_min=None, uper_max=None): ) +def _uper_enc_via_encode_into(cls, *args, **kwargs): + # type: (Type[UPERcodec_Object[Any]], *Any, **Any) -> bytes + enc = UPER_Encoder() + cls.encode_into(enc, *args, **kwargs) + return enc.as_bytes() + + ASN1_Codecs.PER.register_stem(UPERcodec_Object) @@ -736,9 +743,9 @@ def dec_from_decoder(cls, @classmethod def enc(cls, i, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False): # type: (int, Optional[int], Optional[int], Optional[int], bool) -> bytes - enc = UPER_Encoder() - cls.encode_into(enc, i, size_len, uper_min, uper_max, oer_unsigned) - return enc.as_bytes() + return _uper_enc_via_encode_into( + cls, i, size_len, uper_min, uper_max, oer_unsigned, + ) @classmethod def do_dec(cls, @@ -788,9 +795,9 @@ def dec_from_decoder(cls, @classmethod def enc(cls, i, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False): # type: (int, Optional[int], Optional[int], Optional[int], bool) -> bytes - enc = UPER_Encoder() - cls.encode_into(enc, i, size_len, uper_min, uper_max, oer_unsigned) - return enc.as_bytes() + return _uper_enc_via_encode_into( + cls, i, size_len, uper_min, uper_max, oer_unsigned, + ) @classmethod def do_dec(cls, @@ -896,9 +903,9 @@ def dec_from_decoder(cls, @classmethod def enc(cls, _s, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False): # type: (Any, Optional[int], Optional[int], Optional[int], bool) -> bytes - enc = UPER_Encoder() - cls.encode_into(enc, _s, size_len, uper_min, uper_max, oer_unsigned) - return enc.as_bytes() + return _uper_enc_via_encode_into( + cls, _s, size_len, uper_min, uper_max, oer_unsigned, + ) @classmethod def do_dec(cls, @@ -973,9 +980,9 @@ def dec_from_decoder(cls, @classmethod def enc(cls, _s, size_len=0, uper_min=None, uper_max=None, oer_unsigned=False): # type: (Union[str, bytes], Optional[int], Optional[int], Optional[int], bool) -> bytes # noqa: E501 - enc = UPER_Encoder() - cls.encode_into(enc, _s, size_len, uper_min, uper_max, oer_unsigned) - return enc.as_bytes() + return _uper_enc_via_encode_into( + cls, _s, size_len, uper_min, uper_max, oer_unsigned, + ) @classmethod def do_dec(cls, @@ -1181,12 +1188,10 @@ def enc(cls, uper_enum_values=None, ): # type: (int, Optional[int], Optional[int], Optional[int], bool, Optional[List[int]]) -> bytes # noqa: E501 - enc = UPER_Encoder() - cls.encode_into( - enc, i, size_len, uper_min, uper_max, oer_unsigned, + return _uper_enc_via_encode_into( + cls, i, size_len, uper_min, uper_max, oer_unsigned, uper_enum_values=uper_enum_values, ) - return enc.as_bytes() @classmethod def do_dec(cls, diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 42874a56f57..558f288dc79 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -143,11 +143,21 @@ def __init__(self, # network_tag gets useful for ASN1F_CHOICE self.network_tag = int(implicit_tag or explicit_tag or self.ASN1_tag) self.owners = [] # type: List[Type[ASN1_Packet]] + self._uper_kwargs_cache = None # type: Optional[Dict[str, Any]] def register_owner(self, cls): # type: (Type[ASN1_Packet]) -> None self.owners.append(cls) + def _apply_diff_tag(self, diff_tag): + # type: (Optional[int]) -> None + # this implies that flexible_tag was True + if diff_tag is not None: + if self.implicit_tag is not None: + self.implicit_tag = diff_tag + elif self.explicit_tag is not None: + self.explicit_tag = diff_tag + def i2repr(self, pkt, x): # type: (ASN1_Packet, _I) -> str return repr(x) @@ -184,12 +194,7 @@ def m2i(self, pkt, s): _fname=self.name) else: diff_tag = None - if diff_tag is not None: - # this implies that flexible_tag was True - if self.implicit_tag is not None: - self.implicit_tag = diff_tag - elif self.explicit_tag is not None: - self.explicit_tag = diff_tag + self._apply_diff_tag(diff_tag) codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) codec_kwargs = {} # type: Dict[str, Any] if pkt.ASN1_codec == ASN1_Codecs.OER: @@ -325,6 +330,11 @@ def copy(self): def _uper_codec_kwargs(self, size_len=None): # type: (Optional[int]) -> Dict[str, Any] + # These kwargs only depend on attributes set once at __init__ time, + # so the common (no override) case is cached to avoid rebuilding the + # dict on every field access during build/dissect. + if size_len is None and self._uper_kwargs_cache is not None: + return self._uper_kwargs_cache kwargs = { "size_len": (self.size_len if size_len is None else size_len) or 0, "oer_unsigned": self.oer_unsigned, @@ -338,6 +348,8 @@ def _uper_codec_kwargs(self, size_len=None): kwargs["uper_extensible"] = True if self.uper_enum_values is not None: kwargs["uper_enum_values"] = self.uper_enum_values + if size_len is None: + self._uper_kwargs_cache = kwargs return kwargs def _encode_item(self, pkt, item): @@ -653,11 +665,7 @@ def _apply_tagging_dec(self, s, pkt): safe=self.flexible_tag, _fname=pkt.name, ) - if diff_tag is not None: - if self.implicit_tag is not None: - self.implicit_tag = diff_tag - elif self.explicit_tag is not None: - self.explicit_tag = diff_tag + self._apply_diff_tag(diff_tag) return s def _dissect_sequence_children(self, pkt, s): @@ -872,10 +880,6 @@ def m2i_from_decoder(self, pkt, dec): lst.append(item) return lst - def dissect_from_decoder(self, pkt, dec): - # type: (ASN1_Packet, UPER_Decoder) -> None - self.set_val(pkt, self.m2i_from_decoder(pkt, dec)) - def _uper_encode_into(self, enc, pkt, value=None): # type: (UPER_Encoder, ASN1_Packet, Any) -> None if value is None: @@ -920,11 +924,7 @@ def m2i(self, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag, safe=self.flexible_tag) - if diff_tag is not None: - if self.implicit_tag is not None: - self.implicit_tag = diff_tag - elif self.explicit_tag is not None: - self.explicit_tag = diff_tag + self._apply_diff_tag(diff_tag) count, s = OER_unsigned_integer_dec(s) lst = [] for _ in range(count): @@ -953,11 +953,7 @@ def m2i(self, implicit_tag=self.implicit_tag, explicit_tag=self.explicit_tag, safe=self.flexible_tag) - if diff_tag is not None: - if self.implicit_tag is not None: - self.implicit_tag = diff_tag - elif self.explicit_tag is not None: - self.explicit_tag = diff_tag + self._apply_diff_tag(diff_tag) codec = self.ASN1_tag.get_codec(pkt.ASN1_codec) i, s, remain = codec.check_type_check_len(s) lst = [] @@ -1185,27 +1181,16 @@ def __init__(self, name, default, *args, **kwargs): if hasattr(p.ASN1_root, "choices"): root = cast(ASN1F_CHOICE, p.ASN1_root) for k in root.choice_order: - self.choices[k] = root.choices[k] - self.choice_order.append(k) - self.choice_list.append(root.choices[k]) + self._register_choice(k, root.choices[k]) else: - tag = p.ASN1_root.network_tag - self.choices[tag] = p - self.choice_order.append(tag) - self.choice_list.append(p) + self._register_choice(p.ASN1_root.network_tag, p) elif hasattr(p, "ASN1_tag"): if isinstance(p, type): # should be ASN1F_field class - tag = int(p.ASN1_tag) - self.choices[tag] = p - self.choice_order.append(tag) - self.choice_list.append(p) + self._register_choice(int(p.ASN1_tag), p) else: # should be ASN1F_field instance - tag = p.network_tag - self.choices[tag] = p - self.choice_order.append(tag) - self.choice_list.append(p) + self._register_choice(p.network_tag, p) if hasattr(p, "cls"): self.pktchoices[hash(p.cls)] = (p.implicit_tag, p.explicit_tag) # noqa: E501 else: @@ -1214,6 +1199,12 @@ def __init__(self, name, default, *args, **kwargs): tag: idx for idx, tag in enumerate(self.choice_order) } + def _register_choice(self, tag, choice): + # type: (int, _CHOICE_T) -> None + self.choices[tag] = choice + self.choice_order.append(tag) + self.choice_list.append(choice) + def _dissect_choice_payload(self, pkt, choice, payload): # type: (ASN1_Packet, _CHOICE_T, bytes) -> Tuple[ASN1_Object[Any], bytes] if hasattr(choice, "ASN1_root"): @@ -1280,17 +1271,8 @@ def m2i(self, pkt, s): def _choice_tag_for(self, x): # type: (Any) -> Optional[int] - for index, choice in enumerate(self.choice_list): - if isinstance(choice, type) and hasattr(choice, "ASN1_root"): - if isinstance(x, choice): - return self.choice_order[index] - elif isinstance(choice, type) and hasattr(choice, "ASN1_tag"): - if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: - return self.choice_order[index] - elif hasattr(choice, "ASN1_tag"): - if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: - return self.choice_order[index] - return None + index = self._choice_index_for(x) + return None if index is None else self.choice_order[index] def _choice_index_for(self, x): # type: (Any) -> Optional[int] @@ -1298,9 +1280,6 @@ def _choice_index_for(self, x): if isinstance(choice, type) and hasattr(choice, "ASN1_root"): if isinstance(x, choice): return index - elif isinstance(choice, type) and hasattr(choice, "ASN1_tag"): - if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: - return index elif hasattr(choice, "ASN1_tag"): if isinstance(x, ASN1_Object) and x.tag == choice.ASN1_tag: return index @@ -1447,10 +1426,6 @@ def m2i_from_decoder(self, pkt, dec): p.ASN1_root.dissect_from_decoder(p, dec) return p - def dissect_from_decoder(self, pkt, dec): - # type: (ASN1_Packet, UPER_Decoder) -> None - self.set_val(pkt, self.m2i_from_decoder(pkt, dec)) - def _uper_encode_into(self, enc, pkt, value=None): # type: (UPER_Encoder, ASN1_Packet, Any) -> None if value is None: @@ -1472,11 +1447,7 @@ def m2i(self, pkt, s): explicit_tag=self.explicit_tag, safe=self.flexible_tag, _fname=self.name) - if diff_tag is not None: - if self.implicit_tag is not None: - self.implicit_tag = diff_tag - elif self.explicit_tag is not None: - self.explicit_tag = diff_tag + self._apply_diff_tag(diff_tag) if not s: return None, s return self.extract_packet(cls, s, _underlayer=pkt) From 079bb4a3db77de39337383a4a4959add8a0dd020 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Thu, 2 Jul 2026 20:58:12 +0200 Subject: [PATCH 12/15] ETSI ITS ASN.1 packets (UPER): CAM, DENM, IVIM, SPATEM, MAPEM. AI-Assisted: yes (Cursor AI) --- scapy/contrib/automotive/v2x/__init__.py | 25 + scapy/contrib/automotive/v2x/packets.py | 1944 ++++++++++++++++++++++ scapy/tools/generate_its_asn1.py | 947 +++++++++++ test/contrib/automotive/v2x_packets.py | 100 ++ test/scapy/layers/asn1.uts | 8 + 5 files changed, 3024 insertions(+) create mode 100644 scapy/contrib/automotive/v2x/__init__.py create mode 100644 scapy/contrib/automotive/v2x/packets.py create mode 100644 scapy/tools/generate_its_asn1.py create mode 100644 test/contrib/automotive/v2x_packets.py diff --git a/scapy/contrib/automotive/v2x/__init__.py b/scapy/contrib/automotive/v2x/__init__.py new file mode 100644 index 00000000000..b8a561278cf --- /dev/null +++ b/scapy/contrib/automotive/v2x/__init__.py @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +# scapy.contrib.status = library + +""" +ETSI ITS V2X ASN.1 messages (UPER). + +Implements CAM, DENM, IVIM, SPATEM and MAPEM from ETSI TS 102 637 / TS 103 301. + +Load explicitly:: + + load_contrib("automotive.v2x") +""" + +from scapy.contrib.automotive.v2x.packets import ( + CAM, + DENM, + IVIM, + MAPEM, + SPATEM, +) + +__all__ = ["CAM", "DENM", "IVIM", "SPATEM", "MAPEM"] diff --git a/scapy/contrib/automotive/v2x/packets.py b/scapy/contrib/automotive/v2x/packets.py new file mode 100644 index 00000000000..2bb7818af82 --- /dev/null +++ b/scapy/contrib/automotive/v2x/packets.py @@ -0,0 +1,1944 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +# AUTO-GENERATED by scapy/tools/generate_its_asn1.py - DO NOT EDIT +""" +ETSI ITS ASN.1 packets (UPER): CAM, DENM, IVIM, SPATEM, MAPEM. +""" + +from scapy.asn1.asn1 import ASN1_Codecs +from scapy.asn1fields import ( + ASN1F_BIT_STRING, + ASN1F_BOOLEAN, + ASN1F_CHOICE, + ASN1F_ENUMERATED, + ASN1F_FLAGS, + ASN1F_IA5_STRING, + ASN1F_INTEGER, + ASN1F_NULL, + ASN1F_NUMERIC_STRING, + ASN1F_PACKET, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, + ASN1F_STRING, + ASN1F_UTF8_STRING, + ASN1F_optional, +) +from scapy.asn1packet import ASN1_Packet + +class ItsPduHeader(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("protocolVersion", 0, uper_min=0, uper_max=255, oer_unsigned=True), + ASN1F_INTEGER("messageID", 0, uper_min=0, uper_max=255, oer_unsigned=True), + ASN1F_INTEGER("stationID", 0, uper_min=0, uper_max=4294967295, oer_unsigned=True) + ) + + +class DeltaReferencePosition(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("deltaLatitude", 0, uper_min=-131071, uper_max=131072), + ASN1F_INTEGER("deltaLongitude", 0, uper_min=-131071, uper_max=131072), + ASN1F_INTEGER("deltaAltitude", 0, uper_min=-12700, uper_max=12800) + ) + + +class Altitude(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("altitudeValue", 0, uper_min=-100000, uper_max=800001), + ASN1F_ENUMERATED("altitudeConfidence", 0, {0: 'alt-000-01', 1: 'alt-000-02', 2: 'alt-000-05', 3: 'alt-000-10', 4: 'alt-000-20', 5: 'alt-000-50', 6: 'alt-001-00', 7: 'alt-002-00', 8: 'alt-005-00', 9: 'alt-010-00', 10: 'alt-020-00', 11: 'alt-050-00', 12: 'alt-100-00', 13: 'alt-200-00', 14: 'outOfRange', 15: 'unavailable'}) + ) + + +class PosConfidenceEllipse(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("semiMajorConfidence", 0, uper_min=0, uper_max=4095, oer_unsigned=True), + ASN1F_INTEGER("semiMinorConfidence", 0, uper_min=0, uper_max=4095, oer_unsigned=True), + ASN1F_INTEGER("semiMajorOrientation", 0, uper_min=0, uper_max=3601, oer_unsigned=True) + ) + + +class PathPoint(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("pathPosition", DeltaReferencePosition(), DeltaReferencePosition), + ASN1F_optional(ASN1F_INTEGER("pathDeltaTime", None, uper_min=1, uper_max=65535, oer_unsigned=True)) + ) + + +class PtActivation(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("ptActivationType", 0, uper_min=0, uper_max=255, oer_unsigned=True), + ASN1F_STRING("ptActivationData", b'', uper_min=1, uper_max=20) + ) + + +class CauseCode(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("causeCode", 0, uper_min=0, uper_max=255, oer_unsigned=True), + ASN1F_INTEGER("subCauseCode", 0, uper_min=0, uper_max=255, oer_unsigned=True), uper_extensible=True + ) + + +class Curvature(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("curvatureValue", 0, uper_min=-1023, uper_max=1023), + ASN1F_ENUMERATED("curvatureConfidence", 0, {0: 'onePerMeter-0-00002', 1: 'onePerMeter-0-0001', 2: 'onePerMeter-0-0005', 3: 'onePerMeter-0-002', 4: 'onePerMeter-0-01', 5: 'onePerMeter-0-1', 6: 'outOfRange', 7: 'unavailable'}) + ) + + +class Heading(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("headingValue", 0, uper_min=0, uper_max=3601, oer_unsigned=True), + ASN1F_INTEGER("headingConfidence", 1, uper_min=1, uper_max=127, oer_unsigned=True) + ) + + +class ClosedLanes(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_ENUMERATED("innerhardShoulderStatus", 0, {0: 'availableForStopping', 1: 'closed', 2: 'availableForDriving'})), + ASN1F_optional(ASN1F_ENUMERATED("outerhardShoulderStatus", 0, {0: 'availableForStopping', 1: 'closed', 2: 'availableForDriving'})), + ASN1F_optional(ASN1F_BIT_STRING("drivingLaneStatus", None, uper_min=1, uper_max=13)), uper_extensible=True + ) + + +class Speed(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("speedValue", 0, uper_min=0, uper_max=16383, oer_unsigned=True), + ASN1F_INTEGER("speedConfidence", 1, uper_min=1, uper_max=127, oer_unsigned=True) + ) + + +class LongitudinalAcceleration(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("longitudinalAccelerationValue", 0, uper_min=-160, uper_max=161), + ASN1F_INTEGER("longitudinalAccelerationConfidence", 0, uper_min=0, uper_max=102, oer_unsigned=True) + ) + + +class LateralAcceleration(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("lateralAccelerationValue", 0, uper_min=-160, uper_max=161), + ASN1F_INTEGER("lateralAccelerationConfidence", 0, uper_min=0, uper_max=102, oer_unsigned=True) + ) + + +class VerticalAcceleration(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("verticalAccelerationValue", 0, uper_min=-160, uper_max=161), + ASN1F_INTEGER("verticalAccelerationConfidence", 0, uper_min=0, uper_max=102, oer_unsigned=True) + ) + + +class DangerousGoodsExtended(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_ENUMERATED("dangerousGoodsType", 0, {0: 'explosives1', 1: 'explosives2', 2: 'explosives3', 3: 'explosives4', 4: 'explosives5', 5: 'explosives6', 6: 'flammableGases', 7: 'nonFlammableGases', 8: 'toxicGases', 9: 'flammableLiquids', 10: 'flammableSolids', 11: 'substancesLiableToSpontaneousCombustion', 12: 'substancesEmittingFlammableGasesUponContactWithWater', 13: 'oxidizingSubstances', 14: 'organicPeroxides', 15: 'toxicSubstances', 16: 'infectiousSubstances', 17: 'radioactiveMaterial', 18: 'corrosiveSubstances', 19: 'miscellaneousDangerousSubstances'}), + ASN1F_INTEGER("unNumber", 0, uper_min=0, uper_max=9999, oer_unsigned=True), + ASN1F_BOOLEAN("elevatedTemperature", False), + ASN1F_BOOLEAN("tunnelsRestricted", False), + ASN1F_BOOLEAN("limitedQuantity", False), + ASN1F_optional(ASN1F_IA5_STRING("emergencyActionCode", None, uper_min=1, uper_max=24)), + ASN1F_optional(ASN1F_NUMERIC_STRING("phoneNumber", None, uper_min=1, uper_max=16)), + ASN1F_optional(ASN1F_UTF8_STRING("companyName", None, uper_min=1, uper_max=24)), uper_extensible=True + ) + + +class VehicleIdentification(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_IA5_STRING("wMInumber", None, uper_min=1, uper_max=3)), + ASN1F_optional(ASN1F_IA5_STRING("vDS", None, size_len=6)), uper_extensible=True + ) + + +class VehicleLength(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("vehicleLengthValue", 1, uper_min=1, uper_max=1023, oer_unsigned=True), + ASN1F_ENUMERATED("vehicleLengthConfidenceIndication", 0, {0: 'noTrailerPresent', 1: 'trailerPresentWithKnownLength', 2: 'trailerPresentWithUnknownLength', 3: 'trailerPresenceIsUnknown', 4: 'unavailable'}) + ) + + +class SteeringWheelAngle(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("steeringWheelAngleValue", 0, uper_min=-511, uper_max=512), + ASN1F_INTEGER("steeringWheelAngleConfidence", 1, uper_min=1, uper_max=127, oer_unsigned=True) + ) + + +class YawRate(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("yawRateValue", 0, uper_min=-32766, uper_max=32767), + ASN1F_ENUMERATED("yawRateConfidence", 0, {0: 'degSec-000-01', 1: 'degSec-000-05', 2: 'degSec-000-10', 3: 'degSec-001-00', 4: 'degSec-005-00', 5: 'degSec-010-00', 6: 'degSec-100-00', 7: 'outOfRange', 8: 'unavailable'}) + ) + + +class ActionID(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("originatingStationID", 0, uper_min=0, uper_max=4294967295, oer_unsigned=True), + ASN1F_INTEGER("sequenceNumber", 0, uper_min=0, uper_max=65535, oer_unsigned=True) + ) + + +class ProtectedCommunicationZone(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_ENUMERATED("protectedZoneType", 0, {0: 'permanentCenDsrcTolling', 1: 'temporaryCenDsrcTolling'}), + ASN1F_optional(ASN1F_INTEGER("expiryTime", None, uper_min=0, uper_max=4398046511103, oer_unsigned=True)), + ASN1F_INTEGER("protectedZoneLatitude", 0, uper_min=-900000000, uper_max=900000001), + ASN1F_INTEGER("protectedZoneLongitude", 0, uper_min=-1800000000, uper_max=1800000001), + ASN1F_optional(ASN1F_INTEGER("protectedZoneRadius", None, uper_min=1, uper_max=255, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("protectedZoneID", None, uper_min=0, uper_max=134217727, oer_unsigned=True)), uper_extensible=True + ) + + +class EventPoint(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("eventPosition", DeltaReferencePosition(), DeltaReferencePosition), + ASN1F_optional(ASN1F_INTEGER("eventDeltaTime", None, uper_min=1, uper_max=65535, oer_unsigned=True)), + ASN1F_INTEGER("informationQuality", 0, uper_min=0, uper_max=7, oer_unsigned=True) + ) + + +class CenDsrcTollingZone(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("protectedZoneLatitude", 0, uper_min=-900000000, uper_max=900000001), + ASN1F_INTEGER("protectedZoneLongitude", 0, uper_min=-1800000000, uper_max=1800000001), + ASN1F_optional(ASN1F_INTEGER("cenDsrcTollingZoneID", None, uper_min=0, uper_max=134217727, oer_unsigned=True)), uper_extensible=True + ) + + +class BasicVehicleContainerHighFrequency(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("heading", Heading(), Heading), + ASN1F_PACKET("speed", Speed(), Speed), + ASN1F_ENUMERATED("driveDirection", 0, {0: 'forward', 1: 'backward', 2: 'unavailable'}), + ASN1F_PACKET("vehicleLength", VehicleLength(), VehicleLength), + ASN1F_INTEGER("vehicleWidth", 1, uper_min=1, uper_max=62, oer_unsigned=True), + ASN1F_PACKET("longitudinalAcceleration", LongitudinalAcceleration(), LongitudinalAcceleration), + ASN1F_PACKET("curvature", Curvature(), Curvature), + ASN1F_ENUMERATED("curvatureCalculationMode", 0, {0: 'yawRateUsed', 1: 'yawRateNotUsed', 2: 'unavailable'}), + ASN1F_PACKET("yawRate", YawRate(), YawRate), + ASN1F_optional(ASN1F_FLAGS("accelerationControl", None, ['brakePedalEngaged', 'gasPedalEngaged', 'emergencyBrakeEngaged', 'collisionWarningEngaged', 'accEngaged', 'cruiseControlEngaged', 'speedLimiterEngaged'], uper_min=7, uper_max=7)), + ASN1F_optional(ASN1F_INTEGER("lanePosition", None, uper_min=-1, uper_max=14)), + ASN1F_optional(ASN1F_PACKET("steeringWheelAngle", None, SteeringWheelAngle)), + ASN1F_optional(ASN1F_PACKET("lateralAcceleration", None, LateralAcceleration)), + ASN1F_optional(ASN1F_PACKET("verticalAcceleration", None, VerticalAcceleration)), + ASN1F_optional(ASN1F_INTEGER("performanceClass", None, uper_min=0, uper_max=7, oer_unsigned=True)), + ASN1F_optional(ASN1F_PACKET("cenDsrcTollingZone", None, CenDsrcTollingZone)) + ) + + +class BasicVehicleContainerLowFrequency(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_ENUMERATED("vehicleRole", 0, {0: 'default', 1: 'publicTransport', 2: 'specialTransport', 3: 'dangerousGoods', 4: 'roadWork', 5: 'rescue', 6: 'emergency', 7: 'safetyCar', 8: 'agriculture', 9: 'commercial', 10: 'military', 11: 'roadOperator', 12: 'taxi', 13: 'reserved1', 14: 'reserved2', 15: 'reserved3'}), + ASN1F_FLAGS("exteriorLights", b'', ['lowBeamHeadlightsOn', 'highBeamHeadlightsOn', 'leftTurnSignalOn', 'rightTurnSignalOn', 'daytimeRunningLightsOn', 'reverseLightOn', 'fogLightOn', 'parkingLightsOn'], uper_min=8, uper_max=8), + ASN1F_SEQUENCE_OF("pathHistory", [], PathPoint, uper_min=0, uper_max=40) + ) + + +class PublicTransportContainer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BOOLEAN("embarkationStatus", False), + ASN1F_optional(ASN1F_PACKET("ptActivation", None, PtActivation)) + ) + + +class SpecialTransportContainer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_FLAGS("specialTransportType", b'', ['heavyLoad', 'excessWidth', 'excessLength', 'excessHeight'], uper_min=4, uper_max=4), + ASN1F_FLAGS("lightBarSirenInUse", b'', ['lightBarActivated', 'sirenActivated'], uper_min=2, uper_max=2) + ) + + +class DangerousGoodsContainer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_ENUMERATED("dangerousGoodsBasic", 0, {0: 'explosives1', 1: 'explosives2', 2: 'explosives3', 3: 'explosives4', 4: 'explosives5', 5: 'explosives6', 6: 'flammableGases', 7: 'nonFlammableGases', 8: 'toxicGases', 9: 'flammableLiquids', 10: 'flammableSolids', 11: 'substancesLiableToSpontaneousCombustion', 12: 'substancesEmittingFlammableGasesUponContactWithWater', 13: 'oxidizingSubstances', 14: 'organicPeroxides', 15: 'toxicSubstances', 16: 'infectiousSubstances', 17: 'radioactiveMaterial', 18: 'corrosiveSubstances', 19: 'miscellaneousDangerousSubstances'}) + ) + + +class RoadWorksContainerBasic(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_INTEGER("roadworksSubCauseCode", None, uper_min=0, uper_max=255, oer_unsigned=True)), + ASN1F_FLAGS("lightBarSirenInUse", b'', ['lightBarActivated', 'sirenActivated'], uper_min=2, uper_max=2), + ASN1F_optional(ASN1F_PACKET("closedLanes", None, ClosedLanes)) + ) + + +class RescueContainer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_FLAGS("lightBarSirenInUse", b'', ['lightBarActivated', 'sirenActivated'], uper_min=2, uper_max=2) + ) + + +class EmergencyContainer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_FLAGS("lightBarSirenInUse", b'', ['lightBarActivated', 'sirenActivated'], uper_min=2, uper_max=2), + ASN1F_optional(ASN1F_PACKET("incidentIndication", None, CauseCode)), + ASN1F_optional(ASN1F_FLAGS("emergencyPriority", None, ['requestForRightOfWay', 'requestForFreeCrossingAtATrafficLight'], uper_min=2, uper_max=2)) + ) + + +class SafetyCarContainer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_FLAGS("lightBarSirenInUse", b'', ['lightBarActivated', 'sirenActivated'], uper_min=2, uper_max=2), + ASN1F_optional(ASN1F_PACKET("incidentIndication", None, CauseCode)), + ASN1F_optional(ASN1F_ENUMERATED("trafficRule", 0, {0: 'noPassing', 1: 'noPassingForTrucks', 2: 'passToRight', 3: 'passToLeft'})), + ASN1F_optional(ASN1F_INTEGER("speedLimit", None, uper_min=1, uper_max=255, oer_unsigned=True)) + ) + + +class RSUContainerHighFrequency(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_SEQUENCE_OF("protectedCommunicationZonesRSU", None, ProtectedCommunicationZone, uper_min=1, uper_max=16)), uper_extensible=True + ) + + +class SituationContainer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("informationQuality", 0, uper_min=0, uper_max=7, oer_unsigned=True), + ASN1F_PACKET("eventType", CauseCode(), CauseCode), + ASN1F_optional(ASN1F_PACKET("linkedCause", None, CauseCode)), + ASN1F_optional(ASN1F_SEQUENCE_OF("eventHistory", None, EventPoint, uper_min=1, uper_max=23)), uper_extensible=True + ) + + +class LocationContainer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_PACKET("eventSpeed", None, Speed)), + ASN1F_optional(ASN1F_PACKET("eventPositionHeading", None, Heading)), + ASN1F_SEQUENCE_OF("traces", [], ASN1F_STRING, uper_min=1, uper_max=7), + ASN1F_optional(ASN1F_ENUMERATED("roadType", 0, {0: 'urban-NoStructuralSeparationToOppositeLanes', 1: 'urban-WithStructuralSeparationToOppositeLanes', 2: 'nonUrban-NoStructuralSeparationToOppositeLanes', 3: 'nonUrban-WithStructuralSeparationToOppositeLanes'})), uper_extensible=True + ) + + +class ImpactReductionContainer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("heightLonCarrLeft", 1, uper_min=1, uper_max=100, oer_unsigned=True), + ASN1F_INTEGER("heightLonCarrRight", 1, uper_min=1, uper_max=100, oer_unsigned=True), + ASN1F_INTEGER("posLonCarrLeft", 1, uper_min=1, uper_max=127, oer_unsigned=True), + ASN1F_INTEGER("posLonCarrRight", 1, uper_min=1, uper_max=127, oer_unsigned=True), + ASN1F_SEQUENCE_OF("positionOfPillars", [], ASN1F_INTEGER, uper_min=1, uper_max=3, uper_extensible=True), + ASN1F_INTEGER("posCentMass", 1, uper_min=1, uper_max=63, oer_unsigned=True), + ASN1F_INTEGER("wheelBaseVehicle", 1, uper_min=1, uper_max=127, oer_unsigned=True), + ASN1F_INTEGER("turningRadius", 1, uper_min=1, uper_max=255, oer_unsigned=True), + ASN1F_INTEGER("posFrontAx", 1, uper_min=1, uper_max=20, oer_unsigned=True), + ASN1F_FLAGS("positionOfOccupants", b'', ['row1LeftOccupied', 'row1RightOccupied', 'row1MidOccupied', 'row1NotDetectable', 'row1NotPresent', 'row2LeftOccupied', 'row2RightOccupied', 'row2MidOccupied', 'row2NotDetectable', 'row2NotPresent', 'row3LeftOccupied', 'row3RightOccupied', 'row3MidOccupied', 'row3NotDetectable', 'row3NotPresent', 'row4LeftOccupied', 'row4RightOccupied', 'row4MidOccupied', 'row4NotDetectable', 'row4NotPresent'], uper_min=20, uper_max=20), + ASN1F_INTEGER("vehicleMass", 1, uper_min=1, uper_max=1024, oer_unsigned=True), + ASN1F_ENUMERATED("requestResponseIndication", 0, {0: 'request', 1: 'response'}) + ) + + +class StationaryVehicleContainer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_ENUMERATED("stationarySince", 0, {0: 'lessThan1Minute', 1: 'lessThan2Minutes', 2: 'lessThan15Minutes', 3: 'equalOrGreater15Minutes'})), + ASN1F_optional(ASN1F_PACKET("stationaryCause", None, CauseCode)), + ASN1F_optional(ASN1F_PACKET("carryingDangerousGoods", None, DangerousGoodsExtended)), + ASN1F_optional(ASN1F_INTEGER("numberOfOccupants", None, uper_min=0, uper_max=127, oer_unsigned=True)), + ASN1F_optional(ASN1F_PACKET("vehicleIdentification", None, VehicleIdentification)), + ASN1F_optional(ASN1F_FLAGS("energyStorageType", None, ['hydrogenStorage', 'electricEnergyStorage', 'liquidPropaneGas', 'compressedNaturalGas', 'diesel', 'gasoline', 'ammonia'], uper_min=7, uper_max=7)) + ) + + +class EuVehicleCategoryCode(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", None, + ASN1F_ENUMERATED("euVehicleCategoryL", 0, {0: 'l1', 1: 'l2', 2: 'l3', 3: 'l4', 4: 'l5', 5: 'l6', 6: 'l7'}), + ASN1F_ENUMERATED("euVehicleCategoryM", 0, {0: 'm1', 1: 'm2', 2: 'm3'}), + ASN1F_ENUMERATED("euVehicleCategoryN", 0, {0: 'n1', 1: 'n2', 2: 'n3'}), + ASN1F_ENUMERATED("euVehicleCategoryO", 0, {0: 'o1', 1: 'o2', 2: 'o3', 3: 'o4'}), + ASN1F_NULL("euVehilcleCategoryT", None), + ASN1F_NULL("euVehilcleCategoryG", None) + ) + + +class InternationalSign_speedLimits(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_INTEGER("speedLimitMax", None, uper_min=0, uper_max=250, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("speedLimitMin", None, uper_min=0, uper_max=250, oer_unsigned=True)), + ASN1F_INTEGER("unit", 0, uper_min=0, uper_max=15, oer_unsigned=True) + ) + + +class DestinationRoad(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("derType", 0, uper_min=0, uper_max=15, oer_unsigned=True), + ASN1F_optional(ASN1F_INTEGER("roadNumberIdentifier", None, uper_min=1, uper_max=999, oer_unsigned=True)), + ASN1F_optional(ASN1F_UTF8_STRING("roadNumberText", None)) + ) + + +class Distance(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("value", 1, uper_min=1, uper_max=16384, oer_unsigned=True), + ASN1F_INTEGER("unit", 0, uper_min=0, uper_max=15, oer_unsigned=True) + ) + + +class DistanceOrDuration(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("value", 1, uper_min=1, uper_max=16384, oer_unsigned=True), + ASN1F_INTEGER("unit", 0, uper_min=0, uper_max=15, oer_unsigned=True) + ) + + +class HoursMinutes(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("hours", 0, uper_min=0, uper_max=23, oer_unsigned=True), + ASN1F_INTEGER("mins", 0, uper_min=0, uper_max=59, oer_unsigned=True) + ) + + +class MonthDay(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("month", 1, uper_min=1, uper_max=12, oer_unsigned=True), + ASN1F_INTEGER("day", 1, uper_min=1, uper_max=31, oer_unsigned=True) + ) + + +class Weight(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("value", 1, uper_min=1, uper_max=16384, oer_unsigned=True), + ASN1F_INTEGER("unit", 0, uper_min=0, uper_max=15, oer_unsigned=True) + ) + + +class ITS_Inline_InternationalSign_applicablePeriod_year(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("yearRangeStartYear", 2000, uper_min=2000, uper_max=2127, oer_unsigned=True), + ASN1F_INTEGER("yearRangeEndYear", 2000, uper_min=2000, uper_max=2127, oer_unsigned=True) + ) + + +class ITS_Inline_InternationalSign_applicablePeriod_month_day(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("dateRangeStartMonthDay", MonthDay(), MonthDay), + ASN1F_PACKET("dateRangeEndMonthDay", MonthDay(), MonthDay) + ) + + +class ITS_Inline_InternationalSign_applicablePeriod_hourMinutes(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("timeRangeStartTime", HoursMinutes(), HoursMinutes), + ASN1F_PACKET("timeRangeEndTime", HoursMinutes(), HoursMinutes) + ) + + +class ITS_Inline_ITS_Inline_GddStructure_pictogramCode_serviceCategoryCode(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", None, + ASN1F_ENUMERATED("trafficSignPictogram", 0, {0: 'dangerWarning', 1: 'regulatory', 2: 'informative'}), + ASN1F_ENUMERATED("publicFacilitiesPictogram", 0, {0: 'publicFacilities'}), + ASN1F_ENUMERATED("ambientOrRoadConditionPictogram", 0, {0: 'ambientCondition', 1: 'roadCondition'}), uper_extensible=True + ) + + +class ITS_Inline_ITS_Inline_GddStructure_pictogramCode_pictogramCategoryCode(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("nature", 1, uper_min=1, uper_max=9, oer_unsigned=True), + ASN1F_INTEGER("serialNumber", 0, uper_min=0, uper_max=99, oer_unsigned=True) + ) + + +class AxleWeightLimits(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("maxLadenweightOnAxle1", 0, uper_min=0, uper_max=65535, oer_unsigned=True), + ASN1F_INTEGER("maxLadenweightOnAxle2", 0, uper_min=0, uper_max=65535, oer_unsigned=True), + ASN1F_INTEGER("maxLadenweightOnAxle3", 0, uper_min=0, uper_max=65535, oer_unsigned=True), + ASN1F_INTEGER("maxLadenweightOnAxle4", 0, uper_min=0, uper_max=65535, oer_unsigned=True), + ASN1F_INTEGER("maxLadenweightOnAxle5", 0, uper_min=0, uper_max=65535, oer_unsigned=True) + ) + + +class EnvironmentalCharacteristics(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_ENUMERATED("euroValue", 0, {0: 'noEntry', 1: 'euro-1', 2: 'euro-2', 3: 'euro-3', 4: 'euro-4', 5: 'euro-5', 6: 'euro-6', 7: 'reservedForUse1', 8: 'reservedForUse2', 9: 'reservedForUse3', 10: 'reservedForUse4', 11: 'reservedForUse5', 12: 'reservedForUse6', 13: 'reservedForUse7', 14: 'reservedForUse8', 15: 'eev'}), + ASN1F_ENUMERATED("copValue", 0, {0: 'noEntry', 1: 'co2class1', 2: 'co2class2', 3: 'co2class3', 4: 'co2class4', 5: 'co2class5', 6: 'co2class6', 7: 'co2class7', 8: 'reservedforUse'}) + ) + + +class ExhaustEmissionValues(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_ENUMERATED("unitType", 0, {0: 'mg-km', 1: 'mg-kWh'}), + ASN1F_INTEGER("emissionCO", 0, uper_min=0, uper_max=32767, oer_unsigned=True), + ASN1F_INTEGER("emissionHC", 0, uper_min=0, uper_max=65535, oer_unsigned=True), + ASN1F_INTEGER("emissionNOX", 0, uper_min=0, uper_max=65535, oer_unsigned=True), + ASN1F_INTEGER("emissionHCNOX", 0, uper_min=0, uper_max=65535, oer_unsigned=True) + ) + + +class PassengerCapacity(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("numberOfSeats", 0, uper_min=0, uper_max=255, oer_unsigned=True), + ASN1F_INTEGER("numberOfStandingPlaces", 0, uper_min=0, uper_max=255, oer_unsigned=True) + ) + + +class Provider(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_BIT_STRING("countryCode", b'', uper_min=10, uper_max=10), + ASN1F_INTEGER("providerIdentifier", 0, uper_min=0, uper_max=16383, oer_unsigned=True) + ) + + +class SoundLevel(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("soundstationary", 0, uper_min=0, uper_max=255, oer_unsigned=True), + ASN1F_INTEGER("sounddriveby", 0, uper_min=0, uper_max=255, oer_unsigned=True) + ) + + +class VehicleDimensions(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("vehicleLengthOverall", 0, uper_min=0, uper_max=255, oer_unsigned=True), + ASN1F_INTEGER("vehicleHeigthOverall", 0, uper_min=0, uper_max=255, oer_unsigned=True), + ASN1F_INTEGER("vehicleWidthOverall", 0, uper_min=0, uper_max=255, oer_unsigned=True) + ) + + +class VehicleWeightLimits(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("vehicleMaxLadenWeight", 0, uper_min=0, uper_max=65535, oer_unsigned=True), + ASN1F_INTEGER("vehicleTrainMaximumWeight", 0, uper_min=0, uper_max=65535, oer_unsigned=True), + ASN1F_INTEGER("vehicleWeightUnladen", 0, uper_min=0, uper_max=65535, oer_unsigned=True) + ) + + +class ITS_Inline_DieselEmissionValues_particulate(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_ENUMERATED("unitType", 0, {0: 'mg-km', 1: 'mg-kWh'}), + ASN1F_INTEGER("value", 0, uper_min=0, uper_max=32767, oer_unsigned=True) + ) + + +class AdvisorySpeed(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_ENUMERATED("type", 0, {0: 'none', 1: 'greenwave', 2: 'ecoDrive', 3: 'transit'}), + ASN1F_optional(ASN1F_INTEGER("speed", None, uper_min=0, uper_max=500, oer_unsigned=True)), + ASN1F_optional(ASN1F_ENUMERATED("confidence", 0, {0: 'unavailable', 1: 'prec100ms', 2: 'prec10ms', 3: 'prec5ms', 4: 'prec1ms', 5: 'prec0-1ms', 6: 'prec0-05ms', 7: 'prec0-01ms'})), + ASN1F_optional(ASN1F_INTEGER("distance", None, uper_min=0, uper_max=10000, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("class_", None, uper_min=0, uper_max=255, oer_unsigned=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("regional", None, ASN1F_STRING)), uper_extensible=True + ) + + +class ConnectingLane(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("lane", 0, uper_min=0, uper_max=255, oer_unsigned=True), + ASN1F_optional(ASN1F_FLAGS("maneuver", None, ['maneuverStraightAllowed', 'maneuverLeftAllowed', 'maneuverRightAllowed', 'maneuverUTurnAllowed', 'maneuverLeftTurnOnRedAllowed', 'maneuverRightTurnOnRedAllowed', 'maneuverLaneChangeAllowed', 'maneuverNoStoppingAllowed', 'yieldAllwaysRequired', 'goWithHalt', 'caution', 'reserved1'], uper_min=12, uper_max=12)) + ) + + +class ConnectionManeuverAssist(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("connectionID", 0, uper_min=0, uper_max=255, oer_unsigned=True), + ASN1F_optional(ASN1F_INTEGER("queueLength", None, uper_min=0, uper_max=10000, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("availableStorageLength", None, uper_min=0, uper_max=10000, oer_unsigned=True)), + ASN1F_optional(ASN1F_BOOLEAN("waitOnStop", None)), + ASN1F_optional(ASN1F_BOOLEAN("pedBicycleDetect", None)), + ASN1F_optional(ASN1F_SEQUENCE_OF("regional", None, ASN1F_STRING)), uper_extensible=True + ) + + +class DataParameters(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_IA5_STRING("processMethod", None, uper_min=1, uper_max=255)), + ASN1F_optional(ASN1F_IA5_STRING("processAgency", None, uper_min=1, uper_max=255)), + ASN1F_optional(ASN1F_IA5_STRING("lastCheckedDate", None, uper_min=1, uper_max=255)), + ASN1F_optional(ASN1F_IA5_STRING("geoidUsed", None, uper_min=1, uper_max=255)), uper_extensible=True + ) + + +class IntersectionReferenceID(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_INTEGER("region", None, uper_min=0, uper_max=65535, oer_unsigned=True)), + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=65535, oer_unsigned=True) + ) + + +class LaneTypeAttributes(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", None, + ASN1F_FLAGS("vehicle", b'', ['isVehicleRevocableLane', 'isVehicleFlyOverLane', 'hovLaneUseOnly', 'restrictedToBusUse', 'restrictedToTaxiUse', 'restrictedFromPublicUse', 'hasIRbeaconCoverage', 'permissionOnRequest'], uper_min=8, uper_max=8), + ASN1F_FLAGS("crosswalk", b'', ['crosswalkRevocableLane', 'bicyleUseAllowed', 'isXwalkFlyOverLane', 'fixedCycleTime', 'biDirectionalCycleTimes', 'hasPushToWalkButton', 'audioSupport', 'rfSignalRequestPresent', 'unsignalizedSegmentsPresent'], uper_min=16, uper_max=16), + ASN1F_FLAGS("bikeLane", b'', ['bikeRevocableLane', 'pedestrianUseAllowed', 'isBikeFlyOverLane', 'fixedCycleTime', 'biDirectionalCycleTimes', 'isolatedByBarrier', 'unsignalizedSegmentsPresent'], uper_min=16, uper_max=16), + ASN1F_FLAGS("sidewalk", b'', ['sidewalk-RevocableLane', 'bicyleUseAllowed', 'isSidewalkFlyOverLane', 'walkBikes'], uper_min=16, uper_max=16), + ASN1F_FLAGS("median", b'', ['median-RevocableLane', 'median', 'whiteLineHashing', 'stripedLines', 'doubleStripedLines', 'trafficCones', 'constructionBarrier', 'trafficChannels', 'lowCurbs', 'highCurbs'], uper_min=16, uper_max=16), + ASN1F_FLAGS("striping", b'', ['stripeToConnectingLanesRevocableLane', 'stripeDrawOnLeft', 'stripeDrawOnRight', 'stripeToConnectingLanesLeft', 'stripeToConnectingLanesRight', 'stripeToConnectingLanesAhead'], uper_min=16, uper_max=16), + ASN1F_FLAGS("trackedVehicle", b'', ['spec-RevocableLane', 'spec-commuterRailRoadTrack', 'spec-lightRailRoadTrack', 'spec-heavyRailRoadTrack', 'spec-otherRailType'], uper_min=16, uper_max=16), + ASN1F_FLAGS("parking", b'', ['parkingRevocableLane', 'parallelParkingInUse', 'headInParkingInUse', 'doNotParkZone', 'parkingForBusUse', 'parkingForTaxiUse', 'noPublicParkingUse'], uper_min=16, uper_max=16), uper_extensible=True + ) + + +class Node_LLmD_64b(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("lon", 0, uper_min=-1800000000, uper_max=1800000001), + ASN1F_INTEGER("lat", 0, uper_min=-900000000, uper_max=900000001) + ) + + +class Node_XY_20b(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0, uper_min=-512, uper_max=511), + ASN1F_INTEGER("y", 0, uper_min=-512, uper_max=511) + ) + + +class Node_XY_22b(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0, uper_min=-1024, uper_max=1023), + ASN1F_INTEGER("y", 0, uper_min=-1024, uper_max=1023) + ) + + +class Node_XY_24b(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0, uper_min=-2048, uper_max=2047), + ASN1F_INTEGER("y", 0, uper_min=-2048, uper_max=2047) + ) + + +class Node_XY_26b(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0, uper_min=-4096, uper_max=4095), + ASN1F_INTEGER("y", 0, uper_min=-4096, uper_max=4095) + ) + + +class Node_XY_28b(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0, uper_min=-8192, uper_max=8191), + ASN1F_INTEGER("y", 0, uper_min=-8192, uper_max=8191) + ) + + +class Node_XY_32b(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("x", 0, uper_min=-32768, uper_max=32767), + ASN1F_INTEGER("y", 0, uper_min=-32768, uper_max=32767) + ) + + +class NodeOffsetPointXY(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", Node_XY_20b(), + Node_XY_20b, + Node_XY_22b, + Node_XY_24b, + Node_XY_26b, + Node_XY_28b, + Node_XY_32b, + Node_LLmD_64b + ) + + +class Position3D(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("lat", 0, uper_min=-900000000, uper_max=900000001), + ASN1F_INTEGER("long", 0, uper_min=-1800000000, uper_max=1800000001), + ASN1F_optional(ASN1F_INTEGER("elevation", None, uper_min=-4096, uper_max=61439)), + ASN1F_optional(ASN1F_SEQUENCE_OF("regional", None, ASN1F_STRING)), uper_extensible=True + ) + + +class RegulatorySpeedLimit(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_ENUMERATED("type", 0, {0: 'unknown', 1: 'maxSpeedInSchoolZone', 2: 'maxSpeedInSchoolZoneWhenChildrenArePresent', 3: 'maxSpeedInConstructionZone', 4: 'vehicleMinSpeed', 5: 'vehicleMaxSpeed', 6: 'vehicleNightMaxSpeed', 7: 'truckMinSpeed', 8: 'truckMaxSpeed', 9: 'truckNightMaxSpeed', 10: 'vehiclesWithTrailersMinSpeed', 11: 'vehiclesWithTrailersMaxSpeed', 12: 'vehiclesWithTrailersNightMaxSpeed'}), + ASN1F_INTEGER("speed", 0, uper_min=0, uper_max=8191, oer_unsigned=True) + ) + + +class RestrictionUserType(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", None, + ASN1F_ENUMERATED("basicType", 0, {0: 'none', 1: 'equippedTransit', 2: 'equippedTaxis', 3: 'equippedOther', 4: 'emissionCompliant', 5: 'equippedBicycle', 6: 'weightCompliant', 7: 'heightCompliant', 8: 'pedestrians', 9: 'slowMovingPersons', 10: 'wheelchairUsers', 11: 'visualDisabilities', 12: 'audioDisabilities', 13: 'otherUnknownDisabilities'}), uper_extensible=True + ) + + +class RoadSegmentReferenceID(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_INTEGER("region", None, uper_min=0, uper_max=65535, oer_unsigned=True)), + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=65535, oer_unsigned=True) + ) + + +class SignalControlZone(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_STRING("zone", b""), uper_extensible=True + ) + + +class TimeChangeDetails(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_INTEGER("startTime", None, uper_min=0, uper_max=36001, oer_unsigned=True)), + ASN1F_INTEGER("minEndTime", 0, uper_min=0, uper_max=36001, oer_unsigned=True), + ASN1F_optional(ASN1F_INTEGER("maxEndTime", None, uper_min=0, uper_max=36001, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("likelyTime", None, uper_min=0, uper_max=36001, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("confidence", None, uper_min=0, uper_max=15, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("nextTime", None, uper_min=0, uper_max=36001, oer_unsigned=True)) + ) + + +class ITS_Inline_ComputedLane_offsetXaxis(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", None, + ASN1F_INTEGER("small", 0, uper_min=-2047, uper_max=2047), + ASN1F_INTEGER("large", 0, uper_min=-32767, uper_max=32767) + ) + + +class ITS_Inline_ComputedLane_offsetYaxis(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", None, + ASN1F_INTEGER("small", 0, uper_min=-2047, uper_max=2047), + ASN1F_INTEGER("large", 0, uper_min=-32767, uper_max=32767) + ) + + +class IviManagementContainer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("serviceProviderId", Provider(), Provider), + ASN1F_INTEGER("iviIdentificationNumber", 1, uper_min=1, uper_max=32767, oer_unsigned=True), + ASN1F_optional(ASN1F_INTEGER("timeStamp", None, uper_min=0, uper_max=4398046511103, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("validFrom", None, uper_min=0, uper_max=4398046511103, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("validTo", None, uper_min=0, uper_max=4398046511103, oer_unsigned=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("connectedIviStructures", None, ASN1F_INTEGER, uper_min=1, uper_max=8)), + ASN1F_INTEGER("iviStatus", 0, uper_min=0, uper_max=7, oer_unsigned=True), + ASN1F_optional(ASN1F_SEQUENCE_OF("connectedDenms", None, ActionID, uper_min=1, uper_max=8, uper_extensible=True)), uper_extensible=True + ) + + +class MlcPart(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("zoneId", 1, uper_min=1, uper_max=32, oer_unsigned=True), + ASN1F_optional(ASN1F_SEQUENCE_OF("laneIds", None, ASN1F_INTEGER, uper_min=1, uper_max=16, uper_extensible=True)) + ) + + +class AbsolutePosition(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("latitude", 0, uper_min=-900000000, uper_max=900000001), + ASN1F_INTEGER("longitude", 0, uper_min=-1800000000, uper_max=1800000001) + ) + + +class AbsolutePositionWAltitude(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("latitude", 0, uper_min=-900000000, uper_max=900000001), + ASN1F_INTEGER("longitude", 0, uper_min=-1800000000, uper_max=1800000001), + ASN1F_PACKET("altitude", Altitude(), Altitude) + ) + + +class ComputedSegment(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("zoneId", 1, uper_min=1, uper_max=32, oer_unsigned=True), + ASN1F_INTEGER("laneNumber", 0, uper_min=-1, uper_max=14), + ASN1F_INTEGER("laneWidth", 0, uper_min=0, uper_max=1023, oer_unsigned=True), + ASN1F_optional(ASN1F_INTEGER("offsetDistance", None, uper_min=-32768, uper_max=32767)), + ASN1F_optional(ASN1F_PACKET("offsetPosition", None, DeltaReferencePosition)) + ) + + +class DeltaPosition(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("deltaLatitude", 0, uper_min=-131071, uper_max=131072), + ASN1F_INTEGER("deltaLongitude", 0, uper_min=-131071, uper_max=131072) + ) + + +class LaneCharacteristics(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("zoneDefinitionAccuracy", 0, uper_min=0, uper_max=7, oer_unsigned=True), + ASN1F_BOOLEAN("existinglaneMarkingStatus", False), + ASN1F_INTEGER("newlaneMarkingColour", 0, uper_min=0, uper_max=7, oer_unsigned=True), + ASN1F_INTEGER("laneDelimitationLeft", 0, uper_min=0, uper_max=7, oer_unsigned=True), + ASN1F_INTEGER("laneDelimitationRight", 0, uper_min=0, uper_max=7, oer_unsigned=True), + ASN1F_INTEGER("mergingWith", 1, uper_min=1, uper_max=32, oer_unsigned=True) + ) + + +class LayoutComponent(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("layoutComponentId", 1, uper_min=1, uper_max=8, oer_unsigned=True), + ASN1F_INTEGER("height", 10, uper_min=10, uper_max=73, oer_unsigned=True), + ASN1F_INTEGER("width", 10, uper_min=10, uper_max=265, oer_unsigned=True), + ASN1F_INTEGER("x", 10, uper_min=10, uper_max=265, oer_unsigned=True), + ASN1F_INTEGER("y", 10, uper_min=10, uper_max=73, oer_unsigned=True), + ASN1F_INTEGER("textScripting", 0, uper_min=0, uper_max=1, oer_unsigned=True) + ) + + +class LoadType(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("goodsType", 0, uper_min=0, uper_max=15, oer_unsigned=True), + ASN1F_ENUMERATED("dangerousGoodsType", 0, {0: 'explosives1', 1: 'explosives2', 2: 'explosives3', 3: 'explosives4', 4: 'explosives5', 5: 'explosives6', 6: 'flammableGases', 7: 'nonFlammableGases', 8: 'toxicGases', 9: 'flammableLiquids', 10: 'flammableSolids', 11: 'substancesLiableToSpontaneousCombustion', 12: 'substancesEmittingFlammableGasesUponContactWithWater', 13: 'oxidizingSubstances', 14: 'organicPeroxides', 15: 'toxicSubstances', 16: 'infectiousSubstances', 17: 'radioactiveMaterial', 18: 'corrosiveSubstances', 19: 'miscellaneousDangerousSubstances'}), + ASN1F_FLAGS("specialTransportType", b'', ['heavyLoad', 'excessWidth', 'excessLength', 'excessHeight'], uper_min=4, uper_max=4) + ) + + +class MapReference(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", RoadSegmentReferenceID(), + RoadSegmentReferenceID, + IntersectionReferenceID + ) + + +class PolygonalLine(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", None, + ASN1F_STRING("deltaPositions", 0, uper_min=1, uper_max=32, size_len=100), + ASN1F_STRING("deltaPositionsWithAltitude", 0, uper_min=1, uper_max=32, size_len=100), + ASN1F_STRING("absolutePositions", 0, uper_min=1, uper_max=8), + ASN1F_STRING("absolutePositionsWithAltitude", 0, uper_min=1, uper_max=8), uper_extensible=True + ) + + +class RoadSurfaceDynamicCharacteristics(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("condition", 0, uper_min=0, uper_max=15, oer_unsigned=True), + ASN1F_INTEGER("temperature", 0, uper_min=-100, uper_max=151), + ASN1F_INTEGER("iceOrWaterDepth", 0, uper_min=0, uper_max=255, oer_unsigned=True), + ASN1F_INTEGER("treatment", 0, uper_min=0, uper_max=7, oer_unsigned=True) + ) + + +class RoadSurfaceStaticCharacteristics(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("frictionCoefficient", 0, uper_min=0, uper_max=101, oer_unsigned=True), + ASN1F_INTEGER("material", 0, uper_min=0, uper_max=7, oer_unsigned=True), + ASN1F_INTEGER("wear", 0, uper_min=0, uper_max=7, oer_unsigned=True), + ASN1F_INTEGER("avBankingAngle", 0, uper_min=-20, uper_max=21) + ) + + +class Segment(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("line", PolygonalLine(), PolygonalLine), + ASN1F_optional(ASN1F_INTEGER("laneWidth", None, uper_min=0, uper_max=1023, oer_unsigned=True)) + ) + + +class Text(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_INTEGER("layoutComponentId", None, uper_min=1, uper_max=4, oer_unsigned=True)), + ASN1F_BIT_STRING("language", b'', uper_min=10, uper_max=10), + ASN1F_UTF8_STRING("textContent", '') + ) + + +class VehicleCharacteristicsFixValues(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", None, + ASN1F_INTEGER("simpleVehicleType", 0, uper_min=0, uper_max=255, oer_unsigned=True), + EuVehicleCategoryCode, + ASN1F_INTEGER("iso3833VehicleType", 0, uper_min=0, uper_max=255, oer_unsigned=True), + EnvironmentalCharacteristics, + ASN1F_INTEGER("engineCharacteristics", 0, uper_min=0, uper_max=255, oer_unsigned=True), + LoadType, + ASN1F_ENUMERATED("usage", 0, {0: 'default', 1: 'publicTransport', 2: 'specialTransport', 3: 'dangerousGoods', 4: 'roadWork', 5: 'rescue', 6: 'emergency', 7: 'safetyCar', 8: 'agriculture', 9: 'commercial', 10: 'military', 11: 'roadOperator', 12: 'taxi', 13: 'reserved1', 14: 'reserved2', 15: 'reserved3'}), uper_extensible=True + ) + + +class Zone(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", Segment(), + Segment, + PolygonalLine, + ComputedSegment, uper_extensible=True + ) + + +class ITS_Inline_ITS_Inline_ISO14823Code_pictogramCode_serviceCategoryCode(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", None, + ASN1F_ENUMERATED("trafficSignPictogram", 0, {0: 'dangerWarning', 1: 'regulatory', 2: 'informative'}), + ASN1F_ENUMERATED("publicFacilitiesPictogram", 0, {0: 'publicFacilities'}), + ASN1F_ENUMERATED("ambientOrRoadConditionPictogram", 0, {0: 'ambientCondition', 1: 'roadCondition'}), uper_extensible=True + ) + + +class ITS_Inline_ITS_Inline_ISO14823Code_pictogramCode_pictogramCategoryCode(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("nature", 1, uper_min=1, uper_max=9, oer_unsigned=True), + ASN1F_INTEGER("serialNumber", 0, uper_min=0, uper_max=99, oer_unsigned=True) + ) + + +class Ext2(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", None, + ASN1F_INTEGER("content", 16512, uper_min=16512, uper_max=2113663, oer_unsigned=True), + ASN1F_INTEGER("extension", 2113664, uper_min=2113664, uper_max=270549119, oer_unsigned=True) + ) + + +class ReferencePosition(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("latitude", 0, uper_min=-900000000, uper_max=900000001), + ASN1F_INTEGER("longitude", 0, uper_min=-1800000000, uper_max=1800000001), + ASN1F_PACKET("positionConfidenceEllipse", PosConfidenceEllipse(), PosConfidenceEllipse), + ASN1F_PACKET("altitude", Altitude(), Altitude) + ) + + +class HighFrequencyContainer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", BasicVehicleContainerHighFrequency(), + BasicVehicleContainerHighFrequency, + RSUContainerHighFrequency, uper_extensible=True + ) + + +class LowFrequencyContainer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", BasicVehicleContainerLowFrequency(), + BasicVehicleContainerLowFrequency, uper_extensible=True + ) + + +class SpecialVehicleContainer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", PublicTransportContainer(), + PublicTransportContainer, + SpecialTransportContainer, + DangerousGoodsContainer, + RoadWorksContainerBasic, + RescueContainer, + EmergencyContainer, + SafetyCarContainer, uper_extensible=True + ) + + +class BasicContainer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("stationType", 0, uper_min=0, uper_max=255, oer_unsigned=True), + ASN1F_PACKET("referencePosition", ReferencePosition(), ReferencePosition), uper_extensible=True + ) + + +class ManagementContainer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("actionID", ActionID(), ActionID), + ASN1F_INTEGER("detectionTime", 0, uper_min=0, uper_max=4398046511103, oer_unsigned=True), + ASN1F_INTEGER("referenceTime", 0, uper_min=0, uper_max=4398046511103, oer_unsigned=True), + ASN1F_optional(ASN1F_ENUMERATED("termination", 0, {0: 'isCancellation', 1: 'isNegation'})), + ASN1F_PACKET("eventPosition", ReferencePosition(), ReferencePosition), + ASN1F_optional(ASN1F_ENUMERATED("relevanceDistance", 0, {0: 'lessThan50m', 1: 'lessThan100m', 2: 'lessThan200m', 3: 'lessThan500m', 4: 'lessThan1000m', 5: 'lessThan5km', 6: 'lessThan10km', 7: 'over10km'})), + ASN1F_optional(ASN1F_ENUMERATED("relevanceTrafficDirection", 0, {0: 'allTrafficDirections', 1: 'upstreamTraffic', 2: 'downstreamTraffic', 3: 'oppositeTraffic'})), + ASN1F_INTEGER("validityDuration", 0, uper_min=0, uper_max=86400, oer_unsigned=True), + ASN1F_optional(ASN1F_INTEGER("transmissionInterval", None, uper_min=1, uper_max=10000, oer_unsigned=True)), + ASN1F_INTEGER("stationType", 0, uper_min=0, uper_max=255, oer_unsigned=True), uper_extensible=True + ) + + +class RoadWorksContainerExtended(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_FLAGS("lightBarSirenInUse", None, ['lightBarActivated', 'sirenActivated'], uper_min=2, uper_max=2)), + ASN1F_optional(ASN1F_PACKET("closedLanes", None, ClosedLanes)), + ASN1F_optional(ASN1F_SEQUENCE_OF("restriction", None, ASN1F_INTEGER, uper_min=1, uper_max=3, uper_extensible=True)), + ASN1F_optional(ASN1F_INTEGER("speedLimit", None, uper_min=1, uper_max=255, oer_unsigned=True)), + ASN1F_optional(ASN1F_PACKET("incidentIndication", None, CauseCode)), + ASN1F_optional(ASN1F_SEQUENCE_OF("recommendedPath", None, ReferencePosition, uper_min=1, uper_max=40)), + ASN1F_optional(ASN1F_PACKET("startingPointSpeedLimit", None, DeltaReferencePosition)), + ASN1F_optional(ASN1F_ENUMERATED("trafficFlowRule", 0, {0: 'noPassing', 1: 'noPassingForTrucks', 2: 'passToRight', 3: 'passToLeft'})), + ASN1F_optional(ASN1F_SEQUENCE_OF("referenceDenms", None, ActionID, uper_min=1, uper_max=8, uper_extensible=True)) + ) + + +class AlacarteContainer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_INTEGER("lanePosition", None, uper_min=-1, uper_max=14)), + ASN1F_optional(ASN1F_PACKET("impactReduction", None, ImpactReductionContainer)), + ASN1F_optional(ASN1F_INTEGER("externalTemperature", None, uper_min=-60, uper_max=67)), + ASN1F_optional(ASN1F_PACKET("roadWorks", None, RoadWorksContainerExtended)), + ASN1F_optional(ASN1F_ENUMERATED("positioningSolution", 0, {0: 'noPositioningSolution', 1: 'sGNSS', 2: 'dGNSS', 3: 'sGNSSplusDR', 4: 'dGNSSplusDR', 5: 'dR'})), + ASN1F_optional(ASN1F_PACKET("stationaryVehicle", None, StationaryVehicleContainer)), uper_extensible=True + ) + + +class InternationalSign_applicablePeriod(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_PACKET("year", None, ITS_Inline_InternationalSign_applicablePeriod_year)), + ASN1F_optional(ASN1F_PACKET("month_day", None, ITS_Inline_InternationalSign_applicablePeriod_month_day)), + ASN1F_optional(ASN1F_FLAGS("repeatingPeriodDayTypes", None, ['national-holiday', 'even-days', 'odd-days', 'market-day'], uper_min=4, uper_max=4)), + ASN1F_optional(ASN1F_PACKET("hourMinutes", None, ITS_Inline_InternationalSign_applicablePeriod_hourMinutes)), + ASN1F_optional(ASN1F_FLAGS("dateRangeOfWeek", None, ['unused', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'], uper_min=8, uper_max=8)), + ASN1F_optional(ASN1F_PACKET("durationHourMinute", None, HoursMinutes)) + ) + + +class InternationalSign_applicableVehicleDimensions(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_PACKET("vehicleHeight", None, Distance)), + ASN1F_optional(ASN1F_PACKET("vehicleWidth", None, Distance)), + ASN1F_optional(ASN1F_PACKET("vehicleLength", None, Distance)), + ASN1F_optional(ASN1F_PACKET("vehicleWeight", None, Weight)) + ) + + +class InternationalSign_section(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_PACKET("startingPointLength", None, Distance)), + ASN1F_optional(ASN1F_PACKET("continuityLength", None, Distance)) + ) + + +class ITS_Inline_GddStructure_pictogramCode(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_STRING("countryCode", None, size_len=2)), + ASN1F_PACKET("serviceCategoryCode", ITS_Inline_ITS_Inline_GddStructure_pictogramCode_serviceCategoryCode(), ITS_Inline_ITS_Inline_GddStructure_pictogramCode_serviceCategoryCode), + ASN1F_PACKET("pictogramCategoryCode", ITS_Inline_ITS_Inline_GddStructure_pictogramCode_pictogramCategoryCode(), ITS_Inline_ITS_Inline_GddStructure_pictogramCode_pictogramCategoryCode) + ) + + +class DieselEmissionValues(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("particulate", ITS_Inline_DieselEmissionValues_particulate(), ITS_Inline_DieselEmissionValues_particulate), + ASN1F_INTEGER("absorptionCoeff", 0, uper_min=0, uper_max=65535, oer_unsigned=True) + ) + + +class ComputedLane(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("referenceLaneId", 0, uper_min=0, uper_max=255, oer_unsigned=True), + ASN1F_PACKET("offsetXaxis", ITS_Inline_ComputedLane_offsetXaxis(), ITS_Inline_ComputedLane_offsetXaxis), + ASN1F_PACKET("offsetYaxis", ITS_Inline_ComputedLane_offsetYaxis(), ITS_Inline_ComputedLane_offsetYaxis), + ASN1F_optional(ASN1F_INTEGER("rotateXY", None, uper_min=0, uper_max=28800, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("scaleXaxis", None, uper_min=-2048, uper_max=2047)), + ASN1F_optional(ASN1F_INTEGER("scaleYaxis", None, uper_min=-2048, uper_max=2047)), + ASN1F_optional(ASN1F_SEQUENCE_OF("regional", None, ASN1F_STRING)), uper_extensible=True + ) + + +class Connection(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("connectingLane", ConnectingLane(), ConnectingLane), + ASN1F_optional(ASN1F_PACKET("remoteIntersection", None, IntersectionReferenceID)), + ASN1F_optional(ASN1F_INTEGER("signalGroup", None, uper_min=0, uper_max=255, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("userClass", None, uper_min=0, uper_max=255, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("connectionID", None, uper_min=0, uper_max=255, oer_unsigned=True)) + ) + + +class LaneAttributes(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_FLAGS("directionalUse", b'', ['ingressPath', 'egressPath'], uper_min=2, uper_max=2), + ASN1F_FLAGS("sharedWith", b'', ['overlappingLaneDescriptionProvided', 'multipleLanesTreatedAsOneLane', 'otherNonMotorizedTrafficTypes', 'individualMotorizedVehicleTraffic', 'busVehicleTraffic', 'taxiVehicleTraffic', 'pedestriansTraffic', 'cyclistVehicleTraffic', 'trackedVehicleTraffic', 'pedestrianTraffic'], uper_min=10, uper_max=10), + ASN1F_PACKET("laneType", LaneTypeAttributes(), LaneTypeAttributes), + ASN1F_optional(ASN1F_STRING("regional", b"")) + ) + + +class LaneDataAttribute(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", None, + ASN1F_INTEGER("pathEndPointAngle", 0, uper_min=-150, uper_max=150), + ASN1F_INTEGER("laneCrownPointCenter", 0, uper_min=-128, uper_max=127), + ASN1F_INTEGER("laneCrownPointLeft", 0, uper_min=-128, uper_max=127), + ASN1F_INTEGER("laneCrownPointRight", 0, uper_min=-128, uper_max=127), + ASN1F_INTEGER("laneAngle", 0, uper_min=-180, uper_max=180), + ASN1F_STRING("speedLimits", 0, uper_min=1, uper_max=9), uper_extensible=True + ) + + +class MovementEvent(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_ENUMERATED("eventState", 0, {0: 'unavailable', 1: 'dark', 2: 'stop-Then-Proceed', 3: 'stop-And-Remain', 4: 'pre-Movement', 5: 'permissive-Movement-Allowed', 6: 'protected-Movement-Allowed', 7: 'permissive-clearance', 8: 'protected-clearance', 9: 'caution-Conflicting-Traffic'}), + ASN1F_optional(ASN1F_PACKET("timing", None, TimeChangeDetails)), + ASN1F_optional(ASN1F_SEQUENCE_OF("speeds", None, AdvisorySpeed, uper_min=1, uper_max=16)), + ASN1F_optional(ASN1F_SEQUENCE_OF("regional", None, ASN1F_STRING)), uper_extensible=True + ) + + +class MovementState(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_IA5_STRING("movementName", None, uper_min=1, uper_max=63)), + ASN1F_INTEGER("signalGroup", 0, uper_min=0, uper_max=255, oer_unsigned=True), + ASN1F_SEQUENCE_OF("state_time_speed", [MovementEvent()], MovementEvent, uper_min=1, uper_max=16), + ASN1F_optional(ASN1F_SEQUENCE_OF("maneuverAssistList", None, ConnectionManeuverAssist, uper_min=1, uper_max=16)), + ASN1F_optional(ASN1F_SEQUENCE_OF("regional", None, ASN1F_STRING)), uper_extensible=True + ) + + +class NodeAttributeSetXY(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_SEQUENCE_OF("localNode", None, ASN1F_ENUMERATED("item", 0, {0: 'reserved', 1: 'stopLine', 2: 'roundedCapStyleA', 3: 'roundedCapStyleB', 4: 'mergePoint', 5: 'divergePoint', 6: 'downstreamStopLine', 7: 'downstreamStartNode', 8: 'closedToTraffic', 9: 'safeIsland', 10: 'curbPresentAtStepOff', 11: 'hydrantPresent'}), uper_min=1, uper_max=8)), + ASN1F_optional(ASN1F_SEQUENCE_OF("disabled", None, ASN1F_ENUMERATED("item", 0, {0: 'reserved', 1: 'doNotBlock', 2: 'whiteLine', 3: 'mergingLaneLeft', 4: 'mergingLaneRight', 5: 'curbOnLeft', 6: 'curbOnRight', 7: 'loadingzoneOnLeft', 8: 'loadingzoneOnRight', 9: 'turnOutPointOnLeft', 10: 'turnOutPointOnRight', 11: 'adjacentParkingOnLeft', 12: 'adjacentParkingOnRight', 13: 'adjacentBikeLaneOnLeft', 14: 'adjacentBikeLaneOnRight', 15: 'sharedBikeLane', 16: 'bikeBoxInFront', 17: 'transitStopOnLeft', 18: 'transitStopOnRight', 19: 'transitStopInLane', 20: 'sharedWithTrackedVehicle', 21: 'safeIsland', 22: 'lowCurbsPresent', 23: 'rumbleStripPresent', 24: 'audibleSignalingPresent', 25: 'adaptiveTimingPresent', 26: 'rfSignalRequestPresent', 27: 'partialCurbIntrusion', 28: 'taperToLeft', 29: 'taperToRight', 30: 'taperToCenterLine', 31: 'parallelParking', 32: 'headInParking', 33: 'freeParking', 34: 'timeRestrictionsOnParking', 35: 'costToPark', 36: 'midBlockCurbPresent', 37: 'unEvenPavementPresent'}), uper_min=1, uper_max=8)), + ASN1F_optional(ASN1F_SEQUENCE_OF("enabled", None, ASN1F_ENUMERATED("item", 0, {0: 'reserved', 1: 'doNotBlock', 2: 'whiteLine', 3: 'mergingLaneLeft', 4: 'mergingLaneRight', 5: 'curbOnLeft', 6: 'curbOnRight', 7: 'loadingzoneOnLeft', 8: 'loadingzoneOnRight', 9: 'turnOutPointOnLeft', 10: 'turnOutPointOnRight', 11: 'adjacentParkingOnLeft', 12: 'adjacentParkingOnRight', 13: 'adjacentBikeLaneOnLeft', 14: 'adjacentBikeLaneOnRight', 15: 'sharedBikeLane', 16: 'bikeBoxInFront', 17: 'transitStopOnLeft', 18: 'transitStopOnRight', 19: 'transitStopInLane', 20: 'sharedWithTrackedVehicle', 21: 'safeIsland', 22: 'lowCurbsPresent', 23: 'rumbleStripPresent', 24: 'audibleSignalingPresent', 25: 'adaptiveTimingPresent', 26: 'rfSignalRequestPresent', 27: 'partialCurbIntrusion', 28: 'taperToLeft', 29: 'taperToRight', 30: 'taperToCenterLine', 31: 'parallelParking', 32: 'headInParking', 33: 'freeParking', 34: 'timeRestrictionsOnParking', 35: 'costToPark', 36: 'midBlockCurbPresent', 37: 'unEvenPavementPresent'}), uper_min=1, uper_max=8)), + ASN1F_optional(ASN1F_SEQUENCE_OF("data", None, LaneDataAttribute, uper_min=1, uper_max=8)), + ASN1F_optional(ASN1F_INTEGER("dWidth", None, uper_min=-512, uper_max=511)), + ASN1F_optional(ASN1F_INTEGER("dElevation", None, uper_min=-512, uper_max=511)), + ASN1F_optional(ASN1F_SEQUENCE_OF("regional", None, ASN1F_STRING)), uper_extensible=True + ) + + +class NodeXY(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("delta", NodeOffsetPointXY(), NodeOffsetPointXY), + ASN1F_optional(ASN1F_PACKET("attributes", None, NodeAttributeSetXY)), uper_extensible=True + ) + + +class RestrictionClassAssignment(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("id", 0, uper_min=0, uper_max=255, oer_unsigned=True), + ASN1F_SEQUENCE_OF("users", [RestrictionUserType()], RestrictionUserType, uper_min=1, uper_max=16) + ) + + +class GlcPart(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("zoneId", 1, uper_min=1, uper_max=32, oer_unsigned=True), + ASN1F_optional(ASN1F_INTEGER("laneNumber", None, uper_min=-1, uper_max=14)), + ASN1F_optional(ASN1F_INTEGER("zoneExtension", None, uper_min=0, uper_max=255, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("zoneHeading", None, uper_min=0, uper_max=3601, oer_unsigned=True)), + ASN1F_optional(ASN1F_PACKET("zone", None, Zone)), uper_extensible=True + ) + + +class RscPart(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_SEQUENCE_OF("detectionZoneIds", None, ASN1F_INTEGER, uper_min=1, uper_max=8, uper_extensible=True)), + ASN1F_SEQUENCE_OF("relevanceZoneIds", [], ASN1F_INTEGER, uper_min=1, uper_max=8, uper_extensible=True), + ASN1F_optional(ASN1F_INTEGER("direction", None, uper_min=0, uper_max=3, oer_unsigned=True)), + ASN1F_optional(ASN1F_PACKET("roadSurfaceStaticCharacteristics", None, RoadSurfaceStaticCharacteristics)), + ASN1F_optional(ASN1F_PACKET("roadSurfaceDynamicCharacteristics", None, RoadSurfaceDynamicCharacteristics)) + ) + + +class LayoutContainer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("layoutId", 1, uper_min=1, uper_max=4, oer_unsigned=True), + ASN1F_optional(ASN1F_INTEGER("height", None, uper_min=10, uper_max=73, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("width", None, uper_min=10, uper_max=265, oer_unsigned=True)), + ASN1F_SEQUENCE_OF("layoutComponents", [LayoutComponent()], LayoutComponent, uper_min=1, uper_max=4, uper_extensible=True), uper_extensible=True + ) + + +class MapLocationContainer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("reference", MapReference(), MapReference), + ASN1F_SEQUENCE_OF("parts", [MlcPart()], MlcPart, uper_min=1, uper_max=16, uper_extensible=True) + ) + + +class VcCode(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("roadSignClass", 0, uper_min=0, uper_max=7, oer_unsigned=True), + ASN1F_INTEGER("roadSignCode", 1, uper_min=1, uper_max=64, oer_unsigned=True), + ASN1F_INTEGER("vcOption", 0, uper_min=0, uper_max=7, oer_unsigned=True), + ASN1F_optional(ASN1F_SEQUENCE_OF("validity", None, InternationalSign_applicablePeriod, uper_min=1, uper_max=8, uper_extensible=True)), + ASN1F_optional(ASN1F_INTEGER("value", None, uper_min=0, uper_max=65535, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("unit", None, uper_min=0, uper_max=15, oer_unsigned=True)) + ) + + +class ITS_Inline_ISO14823Code_pictogramCode(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_STRING("countryCode", None, size_len=2)), + ASN1F_PACKET("serviceCategoryCode", ITS_Inline_ITS_Inline_ISO14823Code_pictogramCode_serviceCategoryCode(), ITS_Inline_ITS_Inline_ISO14823Code_pictogramCode_serviceCategoryCode), + ASN1F_PACKET("pictogramCategoryCode", ITS_Inline_ITS_Inline_ISO14823Code_pictogramCode_pictogramCategoryCode(), ITS_Inline_ITS_Inline_ISO14823Code_pictogramCode_pictogramCategoryCode) + ) + + +class ITS_Inline_VehicleCharacteristicsRanges_limits(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", None, + ASN1F_INTEGER("numberOfAxles", 0, uper_min=0, uper_max=7, oer_unsigned=True), + VehicleDimensions, + VehicleWeightLimits, + AxleWeightLimits, + PassengerCapacity, + ExhaustEmissionValues, + DieselEmissionValues, + SoundLevel, uper_extensible=True + ) + + +class Ext1(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", None, + ASN1F_INTEGER("content", 128, uper_min=128, uper_max=16511, oer_unsigned=True), + Ext2 + ) + + +class CamParameters(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("basicContainer", BasicContainer(), BasicContainer), + ASN1F_PACKET("highFrequencyContainer", HighFrequencyContainer(), HighFrequencyContainer), + ASN1F_optional(ASN1F_PACKET("lowFrequencyContainer", None, LowFrequencyContainer)), + ASN1F_optional(ASN1F_PACKET("specialVehicleContainer", None, SpecialVehicleContainer)), uper_extensible=True + ) + + +class DecentralizedEnvironmentalNotificationMessage(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("management", ManagementContainer(), ManagementContainer), + ASN1F_optional(ASN1F_PACKET("situation", None, SituationContainer)), + ASN1F_optional(ASN1F_PACKET("location", None, LocationContainer)), + ASN1F_optional(ASN1F_PACKET("alacarte", None, AlacarteContainer)) + ) + + +class IntersectionState(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_IA5_STRING("name_", None, uper_min=1, uper_max=63)), + ASN1F_PACKET("id", IntersectionReferenceID(), IntersectionReferenceID), + ASN1F_INTEGER("revision", 0, uper_min=0, uper_max=127, oer_unsigned=True), + ASN1F_FLAGS("status", b'', ['manualControlIsEnabled', 'stopTimeIsActivated', 'failureFlash', 'preemptIsActive', 'signalPriorityIsActive', 'fixedTimeOperation', 'trafficDependentOperation', 'standbyOperation', 'failureMode', 'off', 'recentMAPmessageUpdate', 'recentChangeInMAPassignedLanesIDsUsed', 'noValidMAPisAvailableAtThisTime', 'noValidSPATisAvailableAtThisTime'], uper_min=16, uper_max=16), + ASN1F_optional(ASN1F_INTEGER("moy", None, uper_min=0, uper_max=527040, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("timeStamp", None, uper_min=0, uper_max=65535, oer_unsigned=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("enabledLanes", None, ASN1F_INTEGER, uper_min=1, uper_max=16)), + ASN1F_SEQUENCE_OF("states", [MovementState()], MovementState, uper_min=1, uper_max=255), + ASN1F_optional(ASN1F_SEQUENCE_OF("maneuverAssistList", None, ConnectionManeuverAssist, uper_min=1, uper_max=16)), + ASN1F_optional(ASN1F_SEQUENCE_OF("regional", None, ASN1F_STRING)), uper_extensible=True + ) + + +class NodeListXY(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", None, + ASN1F_STRING("nodes", 0, uper_min=2, uper_max=63), + ComputedLane, uper_extensible=True + ) + + +class GeographicLocationContainer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("referencePosition", ReferencePosition(), ReferencePosition), + ASN1F_optional(ASN1F_INTEGER("referencePositionTime", None, uper_min=0, uper_max=4398046511103, oer_unsigned=True)), + ASN1F_optional(ASN1F_PACKET("referencePositionHeading", None, Heading)), + ASN1F_optional(ASN1F_PACKET("referencePositionSpeed", None, Speed)), + ASN1F_SEQUENCE_OF("parts", [GlcPart()], GlcPart, uper_min=1, uper_max=16, uper_extensible=True), uper_extensible=True + ) + + +class VehicleCharacteristicsRanges(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("comparisonOperator", 0, uper_min=0, uper_max=3, oer_unsigned=True), + ASN1F_PACKET("limits", ITS_Inline_VehicleCharacteristicsRanges_limits(), ITS_Inline_VehicleCharacteristicsRanges_limits) + ) + + +class VarLengthNumber(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", None, + ASN1F_INTEGER("content", 0, uper_min=0, uper_max=127, oer_unsigned=True), + Ext1 + ) + + +class CoopAwareness(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("generationDeltaTime", 0, uper_min=0, uper_max=65535, oer_unsigned=True), + ASN1F_PACKET("camParameters", CamParameters(), CamParameters) + ) + + +class SPAT(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_INTEGER("timeStamp", None, uper_min=0, uper_max=527040, oer_unsigned=True)), + ASN1F_optional(ASN1F_IA5_STRING("name_", None, uper_min=1, uper_max=63)), + ASN1F_SEQUENCE_OF("intersections", [IntersectionState()], IntersectionState, uper_min=1, uper_max=32), + ASN1F_optional(ASN1F_SEQUENCE_OF("regional", None, ASN1F_STRING)), uper_extensible=True + ) + + +class GenericLane(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("laneID", 0, uper_min=0, uper_max=255, oer_unsigned=True), + ASN1F_optional(ASN1F_IA5_STRING("name_", None, uper_min=1, uper_max=63)), + ASN1F_optional(ASN1F_INTEGER("ingressApproach", None, uper_min=0, uper_max=15, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("egressApproach", None, uper_min=0, uper_max=15, oer_unsigned=True)), + ASN1F_PACKET("laneAttributes", LaneAttributes(), LaneAttributes), + ASN1F_optional(ASN1F_FLAGS("maneuvers", None, ['maneuverStraightAllowed', 'maneuverLeftAllowed', 'maneuverRightAllowed', 'maneuverUTurnAllowed', 'maneuverLeftTurnOnRedAllowed', 'maneuverRightTurnOnRedAllowed', 'maneuverLaneChangeAllowed', 'maneuverNoStoppingAllowed', 'yieldAllwaysRequired', 'goWithHalt', 'caution', 'reserved1'], uper_min=12, uper_max=12)), + ASN1F_PACKET("nodeList", NodeListXY(), NodeListXY), + ASN1F_optional(ASN1F_SEQUENCE_OF("connectsTo", None, Connection, uper_min=1, uper_max=16)), + ASN1F_optional(ASN1F_SEQUENCE_OF("overlays", None, ASN1F_INTEGER, uper_min=1, uper_max=5)), + ASN1F_optional(ASN1F_SEQUENCE_OF("regional", None, ASN1F_STRING)), uper_extensible=True + ) + + +class IntersectionGeometry(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_IA5_STRING("name_", None, uper_min=1, uper_max=63)), + ASN1F_PACKET("id", IntersectionReferenceID(), IntersectionReferenceID), + ASN1F_INTEGER("revision", 0, uper_min=0, uper_max=127, oer_unsigned=True), + ASN1F_PACKET("refPoint", Position3D(), Position3D), + ASN1F_optional(ASN1F_INTEGER("laneWidth", None, uper_min=0, uper_max=32767, oer_unsigned=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("speedLimits", None, RegulatorySpeedLimit, uper_min=1, uper_max=9)), + ASN1F_SEQUENCE_OF("laneSet", [GenericLane()], GenericLane, uper_min=1, uper_max=255), + ASN1F_optional(ASN1F_SEQUENCE_OF("preemptPriorityData", None, SignalControlZone, uper_min=1, uper_max=32)), + ASN1F_optional(ASN1F_SEQUENCE_OF("regional", None, ASN1F_STRING)), uper_extensible=True + ) + + +class RoadSegment(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_IA5_STRING("name_", None, uper_min=1, uper_max=63)), + ASN1F_PACKET("id", RoadSegmentReferenceID(), RoadSegmentReferenceID), + ASN1F_INTEGER("revision", 0, uper_min=0, uper_max=127, oer_unsigned=True), + ASN1F_PACKET("refPoint", Position3D(), Position3D), + ASN1F_optional(ASN1F_INTEGER("laneWidth", None, uper_min=0, uper_max=32767, oer_unsigned=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("speedLimits", None, RegulatorySpeedLimit, uper_min=1, uper_max=9)), + ASN1F_SEQUENCE_OF("roadLaneSet", [GenericLane()], GenericLane, uper_min=1, uper_max=255), + ASN1F_optional(ASN1F_SEQUENCE_OF("regional", None, ASN1F_STRING)), uper_extensible=True + ) + + +class TractorCharacteristics(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_SEQUENCE_OF("equalTo", None, VehicleCharacteristicsFixValues, uper_min=1, uper_max=4, uper_extensible=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("notEqualTo", None, VehicleCharacteristicsFixValues, uper_min=1, uper_max=4, uper_extensible=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("ranges", None, VehicleCharacteristicsRanges, uper_min=1, uper_max=4, uper_extensible=True)) + ) + + +class IVI_TrailerCharacteristics(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_SEQUENCE_OF("equalTo", None, VehicleCharacteristicsFixValues, uper_min=1, uper_max=4, uper_extensible=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("notEqualTo", None, VehicleCharacteristicsFixValues, uper_min=1, uper_max=4, uper_extensible=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("ranges", None, VehicleCharacteristicsRanges, uper_min=1, uper_max=4, uper_extensible=True)) + ) + + +class MapData(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_INTEGER("timeStamp", None, uper_min=0, uper_max=527040, oer_unsigned=True)), + ASN1F_INTEGER("msgIssueRevision", 0, uper_min=0, uper_max=127, oer_unsigned=True), + ASN1F_optional(ASN1F_ENUMERATED("layerType", 0, {0: 'none', 1: 'mixedContent', 2: 'generalMapData', 3: 'intersectionData', 4: 'curveData', 5: 'roadwaySectionData', 6: 'parkingAreaData', 7: 'sharedLaneData'})), + ASN1F_optional(ASN1F_INTEGER("layerID", None, uper_min=0, uper_max=100, oer_unsigned=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("intersections", None, IntersectionGeometry, uper_min=1, uper_max=32)), + ASN1F_optional(ASN1F_SEQUENCE_OF("roadSegments", None, RoadSegment, uper_min=1, uper_max=32)), + ASN1F_optional(ASN1F_PACKET("dataParameters", None, DataParameters)), + ASN1F_optional(ASN1F_SEQUENCE_OF("restrictionList", None, RestrictionClassAssignment, uper_min=1, uper_max=254)), + ASN1F_optional(ASN1F_SEQUENCE_OF("regional", None, ASN1F_STRING)), uper_extensible=True + ) + + +class CompleteVehicleCharacteristics(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_PACKET("tractor", None, TractorCharacteristics)), + ASN1F_optional(ASN1F_SEQUENCE_OF("trailer", None, IVI_TrailerCharacteristics, uper_min=1, uper_max=3)), + ASN1F_optional(ASN1F_PACKET("train", None, TractorCharacteristics)) + ) + + +class LaneInformation(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("laneNumber", 0, uper_min=-1, uper_max=14), + ASN1F_INTEGER("direction", 0, uper_min=0, uper_max=3, oer_unsigned=True), + ASN1F_optional(ASN1F_PACKET("validity", None, InternationalSign_applicablePeriod)), + ASN1F_INTEGER("laneType", 0, uper_min=0, uper_max=31, oer_unsigned=True), + ASN1F_optional(ASN1F_PACKET("laneTypeQualifier", None, CompleteVehicleCharacteristics)), + ASN1F_INTEGER("laneStatus", 0, uper_min=0, uper_max=7, oer_unsigned=True), + ASN1F_optional(ASN1F_INTEGER("laneWidth", None, uper_min=0, uper_max=1023, oer_unsigned=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("detectionZoneIds", None, ASN1F_INTEGER, uper_min=1, uper_max=8, uper_extensible=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("relevanceZoneIds", None, ASN1F_INTEGER, uper_min=1, uper_max=8, uper_extensible=True)), + ASN1F_optional(ASN1F_PACKET("laneCharacteristics", None, LaneCharacteristics)), + ASN1F_optional(ASN1F_PACKET("laneSurfaceStaticCharacteristics", None, RoadSurfaceStaticCharacteristics)), + ASN1F_optional(ASN1F_PACKET("laneSurfaceDynamicCharacteristics", None, RoadSurfaceDynamicCharacteristics)), uper_extensible=True + ) + + +class RccPart(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_SEQUENCE_OF("relevanceZoneIds", [], ASN1F_INTEGER, uper_min=1, uper_max=8, uper_extensible=True), + ASN1F_ENUMERATED("roadType", 0, {0: 'urban-NoStructuralSeparationToOppositeLanes', 1: 'urban-WithStructuralSeparationToOppositeLanes', 2: 'nonUrban-NoStructuralSeparationToOppositeLanes', 3: 'nonUrban-WithStructuralSeparationToOppositeLanes'}), + ASN1F_SEQUENCE_OF("laneConfiguration", [LaneInformation()], LaneInformation, uper_min=1, uper_max=16, uper_extensible=True), uper_extensible=True + ) + + +class TcPart(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_SEQUENCE_OF("detectionZoneIds", None, ASN1F_INTEGER, uper_min=1, uper_max=8, uper_extensible=True)), + ASN1F_SEQUENCE_OF("relevanceZoneIds", [], ASN1F_INTEGER, uper_min=1, uper_max=8, uper_extensible=True), + ASN1F_optional(ASN1F_INTEGER("direction", None, uper_min=0, uper_max=3, oer_unsigned=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("driverAwarenessZoneIds", None, ASN1F_INTEGER, uper_min=1, uper_max=8, uper_extensible=True)), + ASN1F_optional(ASN1F_INTEGER("minimumAwarenessTime", None, uper_min=0, uper_max=255, oer_unsigned=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("applicableLanes", None, ASN1F_INTEGER, uper_min=1, uper_max=8, uper_extensible=True)), + ASN1F_optional(ASN1F_INTEGER("layoutId", None, uper_min=1, uper_max=4, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("preStoredlayoutId", None, uper_min=1, uper_max=64, oer_unsigned=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("text", None, Text, uper_min=1, uper_max=4, uper_extensible=True)), + ASN1F_STRING("data", b''), + ASN1F_INTEGER("iviType", 0, uper_min=0, uper_max=7, oer_unsigned=True), + ASN1F_optional(ASN1F_INTEGER("laneStatus", None, uper_min=0, uper_max=7, oer_unsigned=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("vehicleCharacteristics", None, CompleteVehicleCharacteristics, uper_min=1, uper_max=8, uper_extensible=True)), uper_extensible=True + ) + + +class IviContainer(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", GeographicLocationContainer(), + GeographicLocationContainer, + ASN1F_STRING("giv", 0, uper_min=1, uper_max=16), + ASN1F_STRING("rcc", 0, uper_min=1, uper_max=16), + ASN1F_STRING("tc", 0, uper_min=1, uper_max=16), + LayoutContainer, + ASN1F_STRING("avc", 0, uper_min=1, uper_max=16), + MapLocationContainer, + ASN1F_STRING("rsc", 0, uper_min=1, uper_max=16), uper_extensible=True + ) + + +class IviStructure(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("mandatory", IviManagementContainer(), IviManagementContainer), + ASN1F_optional(ASN1F_SEQUENCE_OF("optional", None, IviContainer, uper_min=1, uper_max=8, uper_extensible=True)) + ) + + +class GddAttribute(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", InternationalSign_applicablePeriod(), + InternationalSign_applicablePeriod, + InternationalSign_applicablePeriod, + ASN1F_INTEGER("dfl", 1, uper_min=1, uper_max=8, oer_unsigned=True), + InternationalSign_applicableVehicleDimensions, + InternationalSign_speedLimits, + ASN1F_INTEGER("roi", 1, uper_min=1, uper_max=32, oer_unsigned=True), + Distance, + ASN1F_STRING("ddd", b''), + InternationalSign_section, + ASN1F_INTEGER("nol", 0, uper_min=0, uper_max=99, oer_unsigned=True) + ) + + +class GddStructure(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("pictogramCode", ITS_Inline_GddStructure_pictogramCode(), ITS_Inline_GddStructure_pictogramCode), + ASN1F_optional(ASN1F_SEQUENCE_OF("attributes", None, GddAttribute, uper_min=1, uper_max=8, uper_extensible=True)) + ) + + +class DestinationPlace(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("destType", 0, uper_min=0, uper_max=15, oer_unsigned=True), + ASN1F_optional(ASN1F_PACKET("destRSCode", None, GddStructure)), + ASN1F_optional(ASN1F_STRING("destBlob", None)), + ASN1F_optional(ASN1F_INTEGER("placeNameIdentification", None, uper_min=1, uper_max=999, oer_unsigned=True)), + ASN1F_optional(ASN1F_UTF8_STRING("placeNameText", None)) + ) + + +class DDD_IO(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("arrowDirection", 0, uper_min=0, uper_max=7, oer_unsigned=True), + ASN1F_optional(ASN1F_SEQUENCE_OF("destPlace", None, DestinationPlace, uper_min=1, uper_max=4, uper_extensible=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("destRoad", None, DestinationRoad, uper_min=1, uper_max=4, uper_extensible=True)), + ASN1F_optional(ASN1F_INTEGER("roadNumberIdentifier", None, uper_min=1, uper_max=999, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("streetName", None, uper_min=1, uper_max=999, oer_unsigned=True)), + ASN1F_optional(ASN1F_UTF8_STRING("streetNameText", None)), + ASN1F_optional(ASN1F_PACKET("distanceToDivergingPoint", None, DistanceOrDuration)), + ASN1F_optional(ASN1F_PACKET("distanceToDestinationPlace", None, DistanceOrDuration)) + ) + + +class InternationalSign_destinationInformation(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_INTEGER("junctionDirection", None, uper_min=1, uper_max=128, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("roundaboutCwDirection", None, uper_min=1, uper_max=128, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("roundaboutCcwDirection", None, uper_min=1, uper_max=128, oer_unsigned=True)), + ASN1F_SEQUENCE_OF("ioList", [DDD_IO()], DDD_IO, uper_min=1, uper_max=8, uper_extensible=True) + ) + + +class ISO14823Attribute(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", InternationalSign_applicablePeriod(), + InternationalSign_applicablePeriod, + InternationalSign_applicablePeriod, + ASN1F_INTEGER("dfl", 1, uper_min=1, uper_max=8, oer_unsigned=True), + InternationalSign_applicableVehicleDimensions, + InternationalSign_speedLimits, + ASN1F_INTEGER("roi", 1, uper_min=1, uper_max=32, oer_unsigned=True), + Distance, + InternationalSign_destinationInformation + ) + + +class ISO14823Code(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("pictogramCode", ITS_Inline_ISO14823Code_pictogramCode(), ITS_Inline_ISO14823Code_pictogramCode), + ASN1F_optional(ASN1F_SEQUENCE_OF("attributes", None, ISO14823Attribute, uper_min=1, uper_max=8, uper_extensible=True)) + ) + + +class AnyCatalogue(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("owner", Provider(), Provider), + ASN1F_INTEGER("version", 0, uper_min=0, uper_max=255, oer_unsigned=True), + ASN1F_INTEGER("pictogramCode", 0, uper_min=0, uper_max=65535, oer_unsigned=True), + ASN1F_optional(ASN1F_INTEGER("value", None, uper_min=0, uper_max=65535, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("unit", None, uper_min=0, uper_max=15, oer_unsigned=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("attributes", None, ISO14823Attribute, uper_min=1, uper_max=8, uper_extensible=True)) + ) + + +class ITS_Inline_RSCode_code(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_CHOICE( + "root", VcCode(), + VcCode, + ISO14823Code, + ASN1F_INTEGER("itisCodes", 0, uper_min=0, uper_max=65535, oer_unsigned=True), + AnyCatalogue, uper_extensible=True + ) + + +class RSCode(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_INTEGER("layoutComponentId", None, uper_min=1, uper_max=4, oer_unsigned=True)), + ASN1F_PACKET("code", ITS_Inline_RSCode_code(), ITS_Inline_RSCode_code) + ) + + +class GicPart(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_SEQUENCE_OF("detectionZoneIds", None, ASN1F_INTEGER, uper_min=1, uper_max=8, uper_extensible=True)), + ASN1F_optional(ASN1F_PACKET("its_Rrid", None, VarLengthNumber)), + ASN1F_optional(ASN1F_SEQUENCE_OF("relevanceZoneIds", None, ASN1F_INTEGER, uper_min=1, uper_max=8, uper_extensible=True)), + ASN1F_optional(ASN1F_INTEGER("direction", None, uper_min=0, uper_max=3, oer_unsigned=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("driverAwarenessZoneIds", None, ASN1F_INTEGER, uper_min=1, uper_max=8, uper_extensible=True)), + ASN1F_optional(ASN1F_INTEGER("minimumAwarenessTime", None, uper_min=0, uper_max=255, oer_unsigned=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("applicableLanes", None, ASN1F_INTEGER, uper_min=1, uper_max=8, uper_extensible=True)), + ASN1F_INTEGER("iviType", 0, uper_min=0, uper_max=7, oer_unsigned=True), + ASN1F_optional(ASN1F_INTEGER("iviPurpose", None, uper_min=0, uper_max=3, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("laneStatus", None, uper_min=0, uper_max=7, oer_unsigned=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("vehicleCharacteristics", None, CompleteVehicleCharacteristics, uper_min=1, uper_max=8, uper_extensible=True)), + ASN1F_optional(ASN1F_INTEGER("driverCharacteristics", None, uper_min=0, uper_max=3, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("layoutId", None, uper_min=1, uper_max=4, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("preStoredlayoutId", None, uper_min=1, uper_max=64, oer_unsigned=True)), + ASN1F_SEQUENCE_OF("roadSignCodes", [RSCode()], RSCode, uper_min=1, uper_max=4, uper_extensible=True), + ASN1F_optional(ASN1F_SEQUENCE_OF("extraText", None, Text, uper_min=1, uper_max=4, uper_extensible=True)), uper_extensible=True + ) + + +class AutomatedVehicleRule(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("priority", 0, uper_min=0, uper_max=2, oer_unsigned=True), + ASN1F_SEQUENCE_OF("allowedSaeAutomationLevels", [], ASN1F_INTEGER, uper_min=1, uper_max=5), + ASN1F_optional(ASN1F_INTEGER("minGapBetweenVehicles", None, uper_min=0, uper_max=255, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("recGapBetweenVehicles", None, uper_min=0, uper_max=255, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("automatedVehicleMaxSpeedLimit", None, uper_min=0, uper_max=16383, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("automatedVehicleMinSpeedLimit", None, uper_min=0, uper_max=16383, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("automatedVehicleSpeedRecommendation", None, uper_min=0, uper_max=16383, oer_unsigned=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("roadSignCodes", None, RSCode, uper_min=1, uper_max=4, uper_extensible=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("extraText", None, Text, uper_min=1, uper_max=4, uper_extensible=True)), uper_extensible=True + ) + + +class PlatooningRule(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_INTEGER("priority", 0, uper_min=0, uper_max=2, oer_unsigned=True), + ASN1F_SEQUENCE_OF("allowedSaeAutomationLevels", [], ASN1F_INTEGER, uper_min=1, uper_max=5), + ASN1F_optional(ASN1F_INTEGER("maxNoOfVehicles", None, uper_min=2, uper_max=64, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("maxLenghtOfPlatoon", None, uper_min=1, uper_max=64, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("minGapBetweenVehicles", None, uper_min=0, uper_max=255, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("platoonMaxSpeedLimit", None, uper_min=0, uper_max=16383, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("platoonMinSpeedLimit", None, uper_min=0, uper_max=16383, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("platoonSpeedRecommendation", None, uper_min=0, uper_max=16383, oer_unsigned=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("roadSignCodes", None, RSCode, uper_min=1, uper_max=4, uper_extensible=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("extraText", None, Text, uper_min=1, uper_max=4, uper_extensible=True)), uper_extensible=True + ) + + +class AvcPart(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_optional(ASN1F_SEQUENCE_OF("detectionZoneIds", None, ASN1F_INTEGER, uper_min=1, uper_max=8, uper_extensible=True)), + ASN1F_SEQUENCE_OF("relevanceZoneIds", [], ASN1F_INTEGER, uper_min=1, uper_max=8, uper_extensible=True), + ASN1F_optional(ASN1F_INTEGER("direction", None, uper_min=0, uper_max=3, oer_unsigned=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("applicableLanes", None, ASN1F_INTEGER, uper_min=1, uper_max=8, uper_extensible=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("vehicleCharacteristics", None, CompleteVehicleCharacteristics, uper_min=1, uper_max=8, uper_extensible=True)), + ASN1F_optional(ASN1F_SEQUENCE_OF("automatedVehicleRules", None, AutomatedVehicleRule, uper_min=1, uper_max=5)), + ASN1F_optional(ASN1F_SEQUENCE_OF("platooningRules", None, PlatooningRule, uper_min=1, uper_max=5)), uper_extensible=True + ) + + +class CAM(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("header", ItsPduHeader(), ItsPduHeader), + ASN1F_PACKET("cam", CoopAwareness(), CoopAwareness) + ) + + +class DENM(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("header", ItsPduHeader(), ItsPduHeader), + ASN1F_PACKET("denm", DecentralizedEnvironmentalNotificationMessage(), DecentralizedEnvironmentalNotificationMessage) + ) + + +class IVIM(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("header", ItsPduHeader(), ItsPduHeader), + ASN1F_PACKET("ivi", IviStructure(), IviStructure) + ) + + +class SPATEM(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("header", ItsPduHeader(), ItsPduHeader), + ASN1F_PACKET("spat", SPAT(), SPAT) + ) + + +class MAPEM(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("header", ItsPduHeader(), ItsPduHeader), + ASN1F_PACKET("map", MapData(), MapData) + ) + + +CAM = CAM +DENM = DENM +IVIM = IVIM +SPATEM = SPATEM +MAPEM = MAPEM + +__all__ = [ + "CAM", + "DENM", + "IVIM", + "SPATEM", + "MAPEM", + "ItsPduHeader", + "DeltaReferencePosition", + "Altitude", + "PosConfidenceEllipse", + "PathPoint", + "PtActivation", + "CauseCode", + "Curvature", + "Heading", + "ClosedLanes", + "Speed", + "LongitudinalAcceleration", + "LateralAcceleration", + "VerticalAcceleration", + "DangerousGoodsExtended", + "VehicleIdentification", + "VehicleLength", + "SteeringWheelAngle", + "YawRate", + "ActionID", + "ProtectedCommunicationZone", + "EventPoint", + "CenDsrcTollingZone", + "BasicVehicleContainerHighFrequency", + "BasicVehicleContainerLowFrequency", + "PublicTransportContainer", + "SpecialTransportContainer", + "DangerousGoodsContainer", + "RoadWorksContainerBasic", + "RescueContainer", + "EmergencyContainer", + "SafetyCarContainer", + "RSUContainerHighFrequency", + "SituationContainer", + "LocationContainer", + "ImpactReductionContainer", + "StationaryVehicleContainer", + "EuVehicleCategoryCode", + "InternationalSign_speedLimits", + "DestinationRoad", + "Distance", + "DistanceOrDuration", + "HoursMinutes", + "MonthDay", + "Weight", + "ITS_Inline_InternationalSign_applicablePeriod_year", + "ITS_Inline_InternationalSign_applicablePeriod_month_day", + "ITS_Inline_InternationalSign_applicablePeriod_hourMinutes", + "ITS_Inline_ITS_Inline_GddStructure_pictogramCode_serviceCategoryCode", + "ITS_Inline_ITS_Inline_GddStructure_pictogramCode_pictogramCategoryCode", + "AxleWeightLimits", + "EnvironmentalCharacteristics", + "ExhaustEmissionValues", + "PassengerCapacity", + "Provider", + "SoundLevel", + "VehicleDimensions", + "VehicleWeightLimits", + "ITS_Inline_DieselEmissionValues_particulate", + "AdvisorySpeed", + "ConnectingLane", + "ConnectionManeuverAssist", + "DataParameters", + "IntersectionReferenceID", + "LaneTypeAttributes", + "Node_LLmD_64b", + "Node_XY_20b", + "Node_XY_22b", + "Node_XY_24b", + "Node_XY_26b", + "Node_XY_28b", + "Node_XY_32b", + "NodeOffsetPointXY", + "Position3D", + "RegulatorySpeedLimit", + "RestrictionUserType", + "RoadSegmentReferenceID", + "SignalControlZone", + "TimeChangeDetails", + "ITS_Inline_ComputedLane_offsetXaxis", + "ITS_Inline_ComputedLane_offsetYaxis", + "IviManagementContainer", + "MlcPart", + "AbsolutePosition", + "AbsolutePositionWAltitude", + "ComputedSegment", + "DeltaPosition", + "LaneCharacteristics", + "LayoutComponent", + "LoadType", + "MapReference", + "PolygonalLine", + "RoadSurfaceDynamicCharacteristics", + "RoadSurfaceStaticCharacteristics", + "Segment", + "Text", + "VehicleCharacteristicsFixValues", + "Zone", + "ITS_Inline_ITS_Inline_ISO14823Code_pictogramCode_serviceCategoryCode", + "ITS_Inline_ITS_Inline_ISO14823Code_pictogramCode_pictogramCategoryCode", + "Ext2", + "ReferencePosition", + "HighFrequencyContainer", + "LowFrequencyContainer", + "SpecialVehicleContainer", + "BasicContainer", + "ManagementContainer", + "RoadWorksContainerExtended", + "AlacarteContainer", + "InternationalSign_applicablePeriod", + "InternationalSign_applicableVehicleDimensions", + "InternationalSign_section", + "ITS_Inline_GddStructure_pictogramCode", + "DieselEmissionValues", + "ComputedLane", + "Connection", + "LaneAttributes", + "LaneDataAttribute", + "MovementEvent", + "MovementState", + "NodeAttributeSetXY", + "NodeXY", + "RestrictionClassAssignment", + "GlcPart", + "RscPart", + "LayoutContainer", + "MapLocationContainer", + "VcCode", + "ITS_Inline_ISO14823Code_pictogramCode", + "ITS_Inline_VehicleCharacteristicsRanges_limits", + "Ext1", + "CamParameters", + "DecentralizedEnvironmentalNotificationMessage", + "IntersectionState", + "NodeListXY", + "GeographicLocationContainer", + "VehicleCharacteristicsRanges", + "VarLengthNumber", + "CoopAwareness", + "SPAT", + "GenericLane", + "IntersectionGeometry", + "RoadSegment", + "TractorCharacteristics", + "IVI_TrailerCharacteristics", + "MapData", + "CompleteVehicleCharacteristics", + "LaneInformation", + "RccPart", + "TcPart", + "IviContainer", + "IviStructure", + "GddAttribute", + "GddStructure", + "DestinationPlace", + "DDD_IO", + "InternationalSign_destinationInformation", + "ISO14823Attribute", + "ISO14823Code", + "AnyCatalogue", + "ITS_Inline_RSCode_code", + "RSCode", + "GicPart", + "AutomatedVehicleRule", + "PlatooningRule", + "AvcPart", + "CAM", + "DENM", + "IVIM", + "SPATEM", + "MAPEM", +] diff --git a/scapy/tools/generate_its_asn1.py b/scapy/tools/generate_its_asn1.py new file mode 100644 index 00000000000..69f496800d7 --- /dev/null +++ b/scapy/tools/generate_its_asn1.py @@ -0,0 +1,947 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +Generate Scapy ASN1_Packet definitions for ETSI ITS messages. + +Requires asn1tools (dev dependency only, not needed at runtime). + +Usage: + python scapy/tools/generate_its_asn1.py +""" + +from __future__ import annotations + +import argparse +import keyword +import re +from pathlib import Path +from typing import Any, Dict, List, Optional, Set, Tuple + +try: + from asn1tools import parse_files +except ImportError as exc: + raise SystemExit( + "asn1tools is required to regenerate ITS packets " + "(pip install asn1tools)" + ) from exc + +ROOT_MESSAGES = ("CAM", "DENM", "IVIM", "SPATEM", "MAPEM") + +PRIMITIVE_TYPES = { + "INTEGER", + "ENUMERATED", + "BOOLEAN", + "NULL", + "BIT STRING", + "OCTET STRING", + "OBJECT IDENTIFIER", + "IA5String", + "UTF8String", + "NumericString", + "PrintableString", + "GeneralizedTime", + "UTCTime", + "DATE", + "EUI64", + "VisibleString", + "TeletexString", + "GraphicString", + "UniversalString", + "BMPString", + "ObjectDescriptor", + "REAL", +} + +STRING_TYPES = { + "IA5String": "ASN1F_IA5_STRING", + "UTF8String": "ASN1F_UTF8_STRING", + "NumericString": "ASN1F_NUMERIC_STRING", + "PrintableString": "ASN1F_PRINTABLE_STRING", + "GeneralizedTime": "ASN1F_GENERALIZED_TIME", + "UTCTime": "ASN1F_UTC_TIME", +} + + +def _asn_dir() -> Path: + return ( + Path(__file__).resolve().parent.parent + / "contrib" / "automotive" / "v2x" / "asn" + ) + + +def asn_file_list() -> List[Path]: + asn = _asn_dir() + return [ + asn / "ITS-Container.asn", + asn / "CAM-PDU-Descriptions.asn", + asn / "DENM-PDU-Descriptions.asn", + asn / "SPATEM-PDU-Descriptions.asn", + asn / "MAPEM-PDU-Descriptions.asn", + asn / "IVIM-PDU-Descriptions.asn", + asn / "iso-patched" / "ISO24534-3_ElectronicRegistrationIdentificationVehicleDataModule-patched.asn", + asn / "iso-patched" / "ISO14823-missing.asn", + asn / "iso-patched" / "ISO14906(2018)EfcDsrcGenericv7-patched.asn", + asn / "iso-patched" / "ISO14906(2018)EfcDsrcApplicationv6-patched.asn", + asn / "ISO-TS-19091-addgrp-C-2018-patched.asn", + asn / "ISO14816_AVIAEINumberingAndDataStructures.asn", + asn / "ISO19321IVIv2.asn", + asn / "ISO_17419_1-1.asn", + ] + + +PACKET_FIELD_RESERVED = frozenset({"name"}) + + +def field_name(name: str) -> str: + if name in PACKET_FIELD_RESERVED: + return name + "_" + return name + + +def sanitize_name(name: str) -> str: + name = re.sub(r"[^0-9a-zA-Z_]", "_", name.replace("-", "_")) + if not name or name[0].isdigit(): + name = "_" + name + if keyword.iskeyword(name): + name = name + "_" + return name + + +def flatten_members(members: List[Any]) -> List[Dict[str, Any]]: + flat: List[Dict[str, Any]] = [] + for member in members: + if member is None: + continue + if isinstance(member, list): + flat.extend(flatten_members(member)) + else: + flat.append(member) + return flat + + +def inline_type_name(parent: str, member_name: str) -> str: + return f"ITS_Inline_{sanitize_name(parent)}_{member_name}" + + +def flatten_choice_members(members: List[Any]) -> List[Dict[str, Any]]: + return flatten_members(members) + + +def is_extensible(members: List[Any]) -> bool: + return any(m is None for m in members) + + +def py_literal(value: Any) -> str: + if isinstance(value, str): + return repr(value) + if isinstance(value, bytes): + return repr(value) + if isinstance(value, dict): + items = ", ".join(f"{py_literal(k)}: {py_literal(v)}" for k, v in value.items()) + return "{" + items + "}" + if isinstance(value, (list, tuple)): + return "[" + ", ".join(py_literal(v) for v in value) + "]" + return repr(value) + + +class ITSASN1Generator: + def __init__(self, spec: Dict[str, Any], compiled_types: Optional[Set[str]] = None) -> None: + self.spec = spec + self.compiled_types = compiled_types + self.class_names: Dict[Tuple[str, str], str] = {} + self.used_py_names: Set[str] = set() + self.import_fields: Set[str] = set() + self.import_asn1: Set[str] = set() + self._assign_class_names() + + def _assign_class_names(self) -> None: + for module, data in self.spec.items(): + for type_name, desc in data.get("types", {}).items(): + if desc.get("parameters"): + continue + if not self._needs_packet(module, type_name, desc): + continue + py_name = sanitize_name(type_name) + if py_name in self.used_py_names: + py_name = sanitize_name(module.split("-")[0]) + "_" + py_name + while py_name in self.used_py_names: + py_name = py_name + "_" + self.class_names[(module, type_name)] = py_name + self.used_py_names.add(py_name) + + def _lookup_descriptor(self, module: str, type_name: str) -> Tuple[str, str, Dict[str, Any]]: + data = self.spec.get(module, {}) + if type_name in data.get("types", {}): + return module, type_name, data["types"][type_name] + for imported_mod, imported_names in data.get("imports", {}).items(): + if type_name in imported_names: + return self._lookup_descriptor(imported_mod, type_name) + raise KeyError(f"type {type_name!r} not found from module {module!r}") + + def _resolve(self, module: str, type_name: str, seen: Optional[Set[Tuple[str, str]]] = None) -> Tuple[str, str, Dict[str, Any]]: + key = (module, type_name) + if seen is None: + seen = set() + if key in seen: + return module, type_name, self.spec[module]["types"][type_name] + seen.add(key) + mod, name, desc = self._lookup_descriptor(module, type_name) + base = desc.get("type") + if base in PRIMITIVE_TYPES or base in ("SEQUENCE", "CHOICE", "SEQUENCE OF"): + return mod, name, desc + return self._resolve(mod, base, seen) + + def _needs_packet(self, module: str, type_name: str, desc: Optional[Dict[str, Any]] = None) -> bool: + if desc is None: + desc = self.spec[module]["types"][type_name] + if desc.get("parameters"): + return False + base = desc.get("type") + if base in ("SEQUENCE", "CHOICE"): + return True + if base in PRIMITIVE_TYPES: + return False + try: + _, _, resolved = self._resolve(module, type_name) + except KeyError: + return False + return resolved.get("type") in ("SEQUENCE", "CHOICE") + + def _class_for(self, module: str, type_name: str) -> str: + mod, name, desc = self._resolve(module, type_name) + if self._needs_packet(mod, name, desc): + return self.class_names[(mod, name)] + raise KeyError(type_name) + + def _size_args(self, desc: Dict[str, Any], field_cls: str = "") -> str: + size = desc.get("size") + if not size: + return "" + parts = [] + for item in size: + if item is None: + continue + if isinstance(item, tuple): + lo, hi = item + if field_cls in ("ASN1F_BIT_STRING", "ASN1F_FLAGS"): + parts.append(f"uper_min={lo}") + parts.append(f"uper_max={hi}") + elif lo == hi: + parts.append(f"size_len={lo}") + else: + parts.append(f"uper_min={lo}") + parts.append(f"uper_max={hi}") + elif isinstance(item, int): + if field_cls in ("ASN1F_BIT_STRING", "ASN1F_FLAGS"): + parts.append(f"uper_min={item}") + parts.append(f"uper_max={item}") + else: + parts.append(f"size_len={item}") + return (", " + ", ".join(parts)) if parts else "" + + def _integer_args(self, desc: Dict[str, Any], enumerated: bool = False) -> str: + args = [] + if enumerated: + return "" + restricted = desc.get("restricted-to") + if restricted: + lo, hi = restricted[0] + args.append(f"uper_min={lo}") + args.append(f"uper_max={hi}") + if lo >= 0: + args.append("oer_unsigned=True") + return (", " + ", ".join(args)) if args else "" + + def _integer_default(self, desc: Dict[str, Any]) -> int: + restricted = desc.get("restricted-to") + if restricted: + lo, hi = restricted[0] + if lo <= 0 <= hi: + return 0 + return lo + return 0 + + def _register_inline_packet(self, module: str, parent: str, member: Dict[str, Any]) -> str: + member_name = sanitize_name(member.get("name") or "inline") + type_name = inline_type_name(sanitize_name(parent), member_name) + py_name = sanitize_name(type_name) + while py_name in self.used_py_names: + py_name = py_name + "_" + key = (module, type_name) + self.class_names[key] = py_name + self.used_py_names.add(py_name) + if module not in self.spec: + self.spec[module] = {"types": {}} + self.spec[module]["types"][type_name] = member + return py_name + + def _is_forward_ref(self, mod: str, name: str, current_key: Optional[Tuple[str, str]], result_order: Optional[List[Tuple[str, str]]]) -> bool: + if current_key is None or result_order is None: + return False + key = (mod, name) + if key not in self.class_names: + return True + try: + return result_order.index(key) > result_order.index(current_key) + except ValueError: + return False + + def _packet_ref(self, member_name: str, mod: str, name: str, tag_args: str, current_key: Optional[Tuple[str, str]], result_order: Optional[List[Tuple[str, str]]], optional: bool = False) -> str: + if self._is_forward_ref(mod, name, current_key, result_order): + self.import_fields.add("ASN1F_STRING") + default = "None" if optional else 'b""' + return f'ASN1F_STRING("{member_name}", {default}{tag_args})' + cls = self.class_names[(mod, name)] + self.import_fields.add("ASN1F_PACKET") + default = "None" if optional else f"{cls}()" + return f'ASN1F_PACKET("{member_name}", {default}, {cls}{tag_args})' + + def _discover_inline_types(self) -> None: + pending = True + while pending: + pending = False + for module, data in self.spec.items(): + for type_name, desc in list(data.get("types", {}).items()): + if desc.get("type") not in ("SEQUENCE", "CHOICE"): + continue + for member in flatten_members(desc.get("members", [])): + if member.get("type") in ("SEQUENCE", "CHOICE"): + _, inline_name = self._inline_key( + module, type_name, member, + ) + if inline_name not in data.get("types", {}): + self._register_inline_packet(module, type_name, member) + pending = True + + def _field_for_member( + self, + module: str, + member: Dict[str, Any], + in_choice: bool = False, + parent: str = "", + current_key: Optional[Tuple[str, str]] = None, + result_order: Optional[List[Tuple[str, str]]] = None, + ) -> str: + if member is None: + return "" + member_name = field_name(sanitize_name(member.get("name") or "extension")) + type_name = member["type"] + optional = member.get("optional") + tag = member.get("tag") + tag_args = "" + if tag and not in_choice: + tag_args = f", implicit_tag={tag['number']}" + if type_name == "SEQUENCE OF": + self.import_fields.add("ASN1F_SEQUENCE_OF") + element = member.get("element", {}) + elem_type = element.get("type") + if elem_type not in PRIMITIVE_TYPES: + try: + emod, ename, edesc = self._resolve(module, elem_type) + if edesc.get("parameters"): + self.import_fields.add("ASN1F_STRING") + field = f'ASN1F_SEQUENCE_OF("{member_name}", None, ASN1F_STRING{tag_args})' + if optional: + self.import_fields.add("ASN1F_optional") + field = f"ASN1F_optional({field})" + return field + except KeyError: + pass + seq_default = self._sequence_of_default( + module, element, member, optional, parent or member_name, + ) + inner = self._sequence_of_element(module, element, parent=parent or member_name) + size_args = self._sequence_of_size_args(member) + field = f'ASN1F_SEQUENCE_OF("{member_name}", {seq_default}, {inner}{size_args}{tag_args})' + if optional: + self.import_fields.add("ASN1F_optional") + field = f"ASN1F_optional({field})" + return field + if type_name in ("SEQUENCE", "CHOICE"): + _, inline_name = self._inline_key(module, parent or member_name, member) + field = self._packet_ref( + member_name, module, inline_name, tag_args, current_key, result_order, + optional=optional, + ) + if optional: + self.import_fields.add("ASN1F_optional") + field = f"ASN1F_optional({field})" + return field + if type_name in PRIMITIVE_TYPES: + desc = member + mod = module + name = member_name + base = type_name + else: + try: + mod, name, desc = self._resolve(module, type_name) + except KeyError: + self.import_fields.add("ASN1F_STRING") + field = f'ASN1F_STRING("{member_name}", b""{tag_args})' + if optional: + self.import_fields.add("ASN1F_optional") + field = f"ASN1F_optional({field})" + return field + base = desc.get("type") + if desc.get("parameters") or ( + base in ("SEQUENCE", "CHOICE") and (mod, name) not in self.class_names + ): + self.import_fields.add("ASN1F_STRING") + field = f'ASN1F_STRING("{member_name}", b""{tag_args})' + if optional: + self.import_fields.add("ASN1F_optional") + field = f"ASN1F_optional({field})" + return field + + if base == "SEQUENCE": + field = self._packet_ref( + member_name, mod, name, tag_args, current_key, result_order, + optional=optional, + ) + elif base == "CHOICE": + field = self._packet_ref( + member_name, mod, name, tag_args, current_key, result_order, + optional=optional, + ) + elif base == "SEQUENCE OF": + element = desc.get("element", member) + elem_type = element.get("type") + if elem_type not in PRIMITIVE_TYPES: + try: + emod, ename, edesc = self._resolve(mod, elem_type) + if edesc.get("parameters"): + self.import_fields.add("ASN1F_SEQUENCE_OF") + self.import_fields.add("ASN1F_STRING") + field = f'ASN1F_SEQUENCE_OF("{member_name}", None, ASN1F_STRING{tag_args})' + if optional: + self.import_fields.add("ASN1F_optional") + field = f"ASN1F_optional({field})" + return field + except KeyError: + pass + if elem_type in PRIMITIVE_TYPES: + elem_desc = element + elem_base = elem_type + else: + elem_mod, elem_name, elem_desc = self._resolve(mod, elem_type) + elem_base = elem_desc.get("type") + self.import_fields.add("ASN1F_SEQUENCE_OF") + seq_default = self._sequence_of_default( + mod, element, desc, optional, type_name, + ) + size_args = self._sequence_of_size_args(desc) + if elem_base == "SEQUENCE": + if elem_type in PRIMITIVE_TYPES: + raise KeyError("unexpected inline SEQUENCE element") + elem_mod, elem_name, _ = self._resolve(mod, elem_type) + if self._is_forward_ref(elem_mod, elem_name, current_key, result_order): + inner = "ASN1F_STRING" + self.import_fields.add("ASN1F_STRING") + else: + inner = self.class_names[(elem_mod, elem_name)] + field = f'ASN1F_SEQUENCE_OF("{member_name}", {seq_default}, {inner}{size_args}{tag_args})' + else: + inner = self._sequence_of_element(mod, element, parent=type_name) + field = f'ASN1F_SEQUENCE_OF("{member_name}", {seq_default}, {inner}{size_args}{tag_args})' + else: + field = self._primitive_field(member_name, desc, tag_args, in_choice, optional=optional) + + if optional: + self.import_fields.add("ASN1F_optional") + field = f"ASN1F_optional({field})" + return field + + def _sequence_of_size_args(self, desc: Dict[str, Any]) -> str: + parts = [] + extensible = False + for item in desc.get("size") or []: + if item is None: + extensible = True + continue + if isinstance(item, tuple): + parts.append(f"uper_min={item[0]}") + parts.append(f"uper_max={item[1]}") + elif isinstance(item, int): + parts.append(f"uper_min={item}") + parts.append(f"uper_max={item}") + if extensible: + parts.append("uper_extensible=True") + return (", " + ", ".join(parts)) if parts else "" + + def _sequence_of_default( + self, + module: str, + element: Dict[str, Any], + desc: Dict[str, Any], + optional: bool, + parent: str, + ) -> str: + if optional: + return "None" + min_size = 0 + for item in desc.get("size") or []: + if item is None: + continue + if isinstance(item, tuple): + min_size = max(min_size, item[0]) + elif isinstance(item, int): + min_size = max(min_size, item) + if min_size <= 0: + return "[]" + inner = self._sequence_of_element(module, element, parent=parent) + if inner.startswith("ASN1F_"): + return "[]" + return f"[{inner}()]" + + def _sequence_of_element(self, module: str, element: Dict[str, Any], parent: str = "seqof") -> str: + elem_type = element.get("type") + if elem_type in ("SEQUENCE", "CHOICE"): + cls = self._register_inline_packet(module, parent, element) + return cls + if elem_type in PRIMITIVE_TYPES: + cls = self._primitive_field_class(element) + self.import_fields.add(cls) + if cls in ("ASN1F_ENUMERATED", "ASN1F_FLAGS"): + return self._primitive_field("item", element, "", False) + return cls + try: + elem_mod, elem_name, elem_desc = self._resolve(module, elem_type) + except KeyError: + self.import_fields.add("ASN1F_STRING") + return "ASN1F_STRING" + if elem_desc.get("parameters"): + self.import_fields.add("ASN1F_STRING") + return "ASN1F_STRING" + if self._needs_packet(elem_mod, elem_name, elem_desc): + return self.class_names[(elem_mod, elem_name)] + cls = self._primitive_field_class(elem_desc) + self.import_fields.add(cls) + if cls in ("ASN1F_ENUMERATED", "ASN1F_FLAGS"): + return self._primitive_field("item", elem_desc, "", False) + return cls + + def _primitive_field_class(self, desc: Dict[str, Any]) -> str: + base = desc.get("type") + if base == "INTEGER": + return "ASN1F_INTEGER" + if base == "ENUMERATED": + return "ASN1F_ENUMERATED" + if base == "BOOLEAN": + return "ASN1F_BOOLEAN" + if base == "NULL": + return "ASN1F_NULL" + if base == "BIT STRING": + if desc.get("named-bits"): + return "ASN1F_FLAGS" + return "ASN1F_BIT_STRING" + if base == "OCTET STRING": + return "ASN1F_STRING" + if base == "OBJECT IDENTIFIER": + return "ASN1F_OID" + if base in STRING_TYPES: + return STRING_TYPES[base] + return "ASN1F_STRING" + + def _enum_mapping(self, desc: Dict[str, Any]) -> Dict[str, int]: + named = desc.get("named-numbers") or desc.get("values") + if not named: + return {} + if isinstance(named, dict): + return {k: v for k, v in named.items() if v is not None} + mapping: Dict[str, int] = {} + for item in named: + if item is None: + continue + k, v = item + if k is not None: + mapping[k] = v + return mapping + + def _enum_default(self, desc: Dict[str, Any]) -> int: + mapping = self._enum_mapping(desc) + if mapping: + return min(mapping.values()) + values = [v for v in (desc.get("values") or []) if v is not None] + if values: + return values[0][1] + restricted = desc.get("restricted-to") + if restricted: + return restricted[0][0] + return 0 + + def _primitive_field(self, member_name: str, desc: Dict[str, Any], tag_args: str, in_choice: bool, optional: bool = False) -> str: + base = desc.get("type") + cls = self._primitive_field_class(desc) + self.import_fields.add(cls) + default: Any + if optional: + default = None + elif base == "BOOLEAN": + default = False + elif base == "NULL": + default = None + elif base in ("OCTET STRING", "IA5String", "UTF8String", "NumericString", "PrintableString"): + default = b"" if base == "OCTET STRING" else "" + elif base == "BIT STRING": + default = b"" + elif base == "INTEGER": + default = self._integer_default(desc) + elif base == "ENUMERATED": + default = self._enum_default(desc) + else: + default = 0 + + extra = self._integer_args(desc, enumerated=(cls == "ASN1F_ENUMERATED")) + self._size_args(desc, cls) + if cls == "ASN1F_ENUMERATED": + mapping = self._enum_mapping(desc) + if mapping: + if default not in mapping.values(): + default = min(mapping.values()) + scapy_enum = {v: k for k, v in mapping.items()} + return f'{cls}("{member_name}", {py_literal(default)}, {py_literal(scapy_enum)}{extra}{tag_args})' + if cls == "ASN1F_FLAGS" and desc.get("named-bits"): + mapping = [bit[0] for bit in desc["named-bits"]] + return f'{cls}("{member_name}", {py_literal(default)}, {py_literal(mapping)}{extra}{tag_args})' + + if in_choice and cls in ("ASN1F_INTEGER", "ASN1F_ENUMERATED", "ASN1F_BOOLEAN", "ASN1F_STRING", "ASN1F_BIT_STRING", "ASN1F_NULL", "ASN1F_OID"): + if tag_args: + tag_num = tag_args.split("=")[-1] + extra += f", implicit_tag={tag_num}" + return f'{cls}("{member_name}", {py_literal(default)}{extra})' + return f'{cls}("{member_name}", {py_literal(default)}{extra}{tag_args})' + + def _choice_root( + self, + module: str, + type_name: str, + desc: Dict[str, Any], + current_key: Optional[Tuple[str, str]] = None, + result_order: Optional[List[Tuple[str, str]]] = None, + ) -> str: + self.import_fields.add("ASN1F_CHOICE") + members = flatten_choice_members(desc.get("members", [])) + alts: List[str] = [] + for member in members: + member_type = member["type"] + if member_type in PRIMITIVE_TYPES: + alts.append(self._primitive_field( + sanitize_name(member.get("name") or "alt"), + member, + "", + in_choice=True, + )) + else: + try: + mod, name, member_desc = self._resolve(module, member_type) + except KeyError: + continue + if member_desc.get("parameters"): + continue + base = member_desc.get("type") + if base in ("SEQUENCE", "CHOICE"): + if (mod, name) not in self.class_names: + continue + if self._is_forward_ref(mod, name, current_key, result_order): + self.import_fields.add("ASN1F_STRING") + alts.append( + self._primitive_field( + sanitize_name(member.get("name") or "alt"), + {"type": "OCTET STRING"}, + "", + in_choice=True, + ) + ) + else: + alts.append(self.class_names[(mod, name)]) + else: + alts.append(self._primitive_field( + sanitize_name(member.get("name") or "alt"), + member_desc, + "", + in_choice=True, + )) + if not alts: + self.import_fields.add("ASN1F_NULL") + alts.append('ASN1F_NULL("unsupported", None)') + default = "None" + if alts: + first = alts[0] + if first[0].isupper() and not first.startswith("ASN1F_"): + default = f"{first}()" + ext_args = ", uper_extensible=True" if is_extensible(desc.get("members", [])) else "" + return "ASN1F_CHOICE(\n " + f'"root", {default},\n ' + ",\n ".join(alts) + ext_args + "\n )" + + def _sequence_root( + self, + module: str, + type_name: str, + desc: Dict[str, Any], + current_key: Optional[Tuple[str, str]] = None, + result_order: Optional[List[Tuple[str, str]]] = None, + ) -> str: + self.import_fields.add("ASN1F_SEQUENCE") + fields = [] + for member in flatten_members(desc.get("members", [])): + field = self._field_for_member( + module, member, parent=type_name, + current_key=current_key, result_order=result_order, + ) + if field: + fields.append(field) + if not fields: + fields.append('ASN1F_NULL("placeholder", None)') + self.import_fields.add("ASN1F_NULL") + ext_args = ", uper_extensible=True" if is_extensible(desc.get("members", [])) else "" + return "ASN1F_SEQUENCE(\n " + ",\n ".join(fields) + ext_args + "\n )" + + def _packet_class( + self, + module: str, + type_name: str, + current_key: Optional[Tuple[str, str]] = None, + result_order: Optional[List[Tuple[str, str]]] = None, + ) -> str: + desc = self.spec[module]["types"][type_name] + py_name = self.class_names[(module, type_name)] + key = current_key or (module, type_name) + if desc["type"] == "CHOICE": + root = self._choice_root(module, type_name, desc, key, result_order) + else: + root = self._sequence_root(module, type_name, desc, key, result_order) + return ( + f"class {py_name}(ASN1_Packet):\n" + f" ASN1_codec = ASN1_Codecs.PER\n" + f" ASN1_root = {root}\n" + ) + + def _reachable_type_keys(self) -> Set[Tuple[str, str]]: + roots: List[Tuple[str, str]] = [] + for root in ROOT_MESSAGES: + for module, data in self.spec.items(): + if root in data.get("types", {}): + roots.append((module, root)) + break + seen: Set[Tuple[str, str]] = set() + queue = list(roots) + while queue: + key = queue.pop(0) + if key in seen or key not in self.class_names: + continue + seen.add(key) + module, type_name = key + desc = self.spec[module]["types"][type_name] + for dep in self._collect_packet_refs(module, desc, type_name): + if dep in self.class_names and dep not in seen: + queue.append(dep) + return seen + + def generate(self) -> str: + self._discover_inline_types() + reachable = self._reachable_type_keys() + self.class_names = { + key: name for key, name in self.class_names.items() + if key in reachable + } + self.used_py_names = set(self.class_names.values()) + ordered: List[Tuple[str, str]] = [] + for module, data in self.spec.items(): + for type_name, desc in data.get("types", {}).items(): + key = (module, type_name) + if key in reachable and self._needs_packet(module, type_name, desc): + ordered.append(key) + + # Topological order: referenced packets before users + deps: Dict[Tuple[str, str], Set[Tuple[str, str]]] = {k: set() for k in ordered} + for key in ordered: + module, type_name = key + desc = self.spec[module]["types"][type_name] + refs = self._collect_packet_refs(module, desc, type_name) + deps[key] = refs + + result_order: List[Tuple[str, str]] = [] + remaining = list(ordered) + while remaining: + progressed = False + for key in list(remaining): + if all( + dep not in remaining or dep in result_order + for dep in deps.get(key, ()) + ): + result_order.append(key) + remaining.remove(key) + progressed = True + if not progressed: + ready = [ + k for k in remaining + if all(dep not in remaining for dep in deps.get(k, ())) + ] + if ready: + key = ready[0] + else: + key = max( + remaining, + key=lambda k: ( + sum(1 for dep in deps.get(k, ()) if dep in result_order), + -sum(1 for dep in deps.get(k, ()) if dep in remaining), + ), + ) + result_order.append(key) + remaining.remove(key) + + root_keys: List[Tuple[str, str]] = [] + for root in ROOT_MESSAGES: + for module, data in self.spec.items(): + if root in data.get("types", {}): + key = (module, root) + if key in result_order: + result_order.remove(key) + root_keys.append(key) + break + result_order.extend(root_keys) + + class_lines: List[str] = [] + for module, type_name in result_order: + class_lines.append( + self._packet_class(module, type_name, (module, type_name), result_order) + ) + class_lines.append("") + + lines = [ + "# SPDX-License-Identifier: GPL-2.0-only", + "# This file is part of Scapy", + "# See https://scapy.net/ for more information", + "# AUTO-GENERATED by scapy/tools/generate_its_asn1.py - DO NOT EDIT", + '"""', + "ETSI ITS ASN.1 packets (UPER): CAM, DENM, IVIM, SPATEM, MAPEM.", + '"""', + "", + "from scapy.asn1.asn1 import ASN1_Codecs", + ] + field_imports = sorted(self.import_fields) + if field_imports: + lines.append("from scapy.asn1fields import (") + for name in field_imports: + lines.append(f" {name},") + lines.append(")") + lines.append("from scapy.asn1packet import ASN1_Packet") + lines.append("") + lines.extend(class_lines) + + for root in ROOT_MESSAGES: + for module, data in self.spec.items(): + if root in data.get("types", {}): + cls = self.class_names[(module, root)] + lines.append(f"{root} = {cls}") + break + lines.append("") + lines.append("__all__ = [") + for root in ROOT_MESSAGES: + lines.append(f' "{root}",') + for module, type_name in result_order: + lines.append(f' "{self.class_names[(module, type_name)]}",') + lines.append("]") + lines.append("") + return "\n".join(lines) + + def _inline_key(self, module: str, parent: str, member: Dict[str, Any]) -> Tuple[str, str]: + member_name = sanitize_name(member.get("name") or "inline") + type_name = inline_type_name(sanitize_name(parent), member_name) + return module, type_name + + def _collect_packet_refs( + self, + module: str, + desc: Dict[str, Any], + parent_name: str = "", + ) -> Set[Tuple[str, str]]: + refs: Set[Tuple[str, str]] = set() + + def add_type_ref(mod: str, type_name: str) -> None: + if type_name in PRIMITIVE_TYPES or type_name in ("SEQUENCE", "CHOICE", "SEQUENCE OF"): + return + try: + rmod, rname, rdesc = self._resolve(mod, type_name) + except KeyError: + return + if rdesc.get("type") == "SEQUENCE OF": + element = rdesc.get("element", {}) + elem_type = element.get("type") + if elem_type in ("SEQUENCE", "CHOICE"): + return + add_type_ref(rmod, elem_type) + return + if self._needs_packet(rmod, rname, rdesc): + refs.add((rmod, rname)) + + def walk_members(members: List[Dict[str, Any]], mod: str, parent: str) -> None: + for member in members: + if not member: + continue + mtype = member.get("type") + if mtype in ("SEQUENCE", "CHOICE"): + key = self._inline_key(mod, parent, member) + if key in self.class_names: + refs.add(key) + inline_desc = self.spec[mod]["types"].get(key[1]) + if inline_desc: + refs.update(self._collect_packet_refs( + mod, inline_desc, key[1], + )) + continue + if mtype == "SEQUENCE OF": + element = member.get("element", {}) + elem_type = element.get("type") + if elem_type in ("SEQUENCE", "CHOICE"): + continue + add_type_ref(mod, elem_type) + elif mtype not in PRIMITIVE_TYPES: + add_type_ref(mod, mtype) + + base = desc.get("type") + if base == "SEQUENCE": + walk_members( + flatten_members(desc.get("members", [])), + module, + parent_name, + ) + elif base == "CHOICE": + walk_members( + flatten_choice_members(desc.get("members", [])), + module, + parent_name, + ) + return refs + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "-o", + "--output", + type=Path, + default=Path(__file__).resolve().parent.parent + / "contrib" / "automotive" / "v2x" / "packets.py", + help="Output Python file", + ) + args = parser.parse_args() + + files = asn_file_list() + missing = [str(f) for f in files if not f.exists()] + if missing: + raise SystemExit("Missing ASN.1 files:\n " + "\n ".join(missing)) + + spec = parse_files([str(f) for f in files]) + import asn1tools + compiled = asn1tools.compile_files([str(f) for f in files], codec="uper") + generator = ITSASN1Generator(spec, compiled_types=set(compiled.types)) + output = generator.generate() + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(output, encoding="utf-8") + print(f"Wrote {args.output} ({output.count(chr(10))} lines)") + + +if __name__ == "__main__": + main() diff --git a/test/contrib/automotive/v2x_packets.py b/test/contrib/automotive/v2x_packets.py new file mode 100644 index 00000000000..335cd4aee81 --- /dev/null +++ b/test/contrib/automotive/v2x_packets.py @@ -0,0 +1,100 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +ETSI ITS V2X ASN.1 packet tests. +""" + +from pathlib import Path + +from scapy.contrib.automotive.v2x import CAM, DENM, IVIM, MAPEM, SPATEM +from scapy.contrib.automotive.v2x.packets import ItsPduHeader +from scapy.packet import raw + +try: + import asn1tools + HAS_ASN1TOOLS = True +except ImportError: + asn1tools = None # type: ignore + HAS_ASN1TOOLS = False + + +def _its_asn_files(): + # type: () -> list + asn = ( + Path(__file__).resolve().parents[3] + / "scapy" / "contrib" / "automotive" / "v2x" / "asn" + ) + return [ + asn / "ITS-Container.asn", + asn / "CAM-PDU-Descriptions.asn", + asn / "DENM-PDU-Descriptions.asn", + asn / "SPATEM-PDU-Descriptions.asn", + asn / "MAPEM-PDU-Descriptions.asn", + asn / "IVIM-PDU-Descriptions.asn", + asn / "iso-patched" / ( + "ISO24534-3_ElectronicRegistrationIdentification" + "VehicleDataModule-patched.asn" + ), + asn / "iso-patched" / "ISO14823-missing.asn", + asn / "iso-patched" / "ISO14906(2018)EfcDsrcGenericv7-patched.asn", + asn / "iso-patched" / "ISO14906(2018)EfcDsrcApplicationv6-patched.asn", + asn / "ISO-TS-19091-addgrp-C-2018-patched.asn", + asn / "ISO14816_AVIAEINumberingAndDataStructures.asn", + asn / "ISO19321IVIv2.asn", + asn / "ISO_17419_1-1.asn", + ] + + +def check_its_import(): + # type: () -> None + assert CAM.__name__ == "CAM" + assert DENM.__name__ == "DENM" + assert IVIM.__name__ == "IVIM" + assert SPATEM.__name__ == "SPATEM" + assert MAPEM.__name__ == "MAPEM" + + +def check_its_messages_build(): + # type: () -> None + for cls, message_id in [ + (CAM, 2), + (DENM, 1), + (IVIM, 6), + (SPATEM, 4), + (MAPEM, 5), + ]: + pkt = cls() + pkt.header = ItsPduHeader( + protocolVersion=1, + messageID=message_id, + stationID=42, + ) + assert len(raw(pkt)) > 0 + + +def check_its_messages_asn1tools_decode(): + # type: () -> None + if not HAS_ASN1TOOLS: + return + compiled = asn1tools.compile_files( + [str(f) for f in _its_asn_files()], + codec="uper", + ) + for name, cls, message_id in [ + ("CAM", CAM, 2), + ("DENM", DENM, 1), + ("IVIM", IVIM, 6), + ("SPATEM", SPATEM, 4), + ("MAPEM", MAPEM, 5), + ]: + pkt = cls() + pkt.header = ItsPduHeader( + protocolVersion=1, + messageID=message_id, + stationID=42, + ) + data = raw(pkt) + decoded = compiled.decode(name, data) + assert decoded["header"]["stationID"] == 42 diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index ec5e835ff21..4f54e10b581 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -454,6 +454,14 @@ __import__('test.scapy.layers.uper_packets', fromlist=['check_uper_sequence_of_c = UPER signed integer field __import__('test.scapy.layers.uper_packets', fromlist=['check_uper_signed_integer']).check_uper_signed_integer() ++ ETSI ITS V2X packets += ITS layer import +__import__('test.contrib.automotive.v2x_packets', fromlist=['check_its_import']).check_its_import() += ITS messages build +__import__('test.contrib.automotive.v2x_packets', fromlist=['check_its_messages_build']).check_its_messages_build() += ITS messages asn1tools decode +__import__('test.contrib.automotive.v2x_packets', fromlist=['check_its_messages_asn1tools_decode']).check_its_messages_asn1tools_decode() + + ASN.1 UPER fuzzing = UPER fuzz encode __import__('test.scapy.layers.uper_fuzz', fromlist=['check_uper_fuzz_encode']).check_uper_fuzz_encode() From c1a3bc564be9bdf5be84e4d00b762737403e1bf3 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 3 Jul 2026 08:19:25 +0200 Subject: [PATCH 13/15] more tests AI-Assisted: yes (Cursor AI) --- scapy/contrib/automotive/v2x/packets.py | 20 +- test/contrib/automotive/v2x_packets.py | 310 +++++++++++++++++++++++- test/scapy/layers/asn1.uts | 16 ++ tox.ini | 4 +- 4 files changed, 344 insertions(+), 6 deletions(-) diff --git a/scapy/contrib/automotive/v2x/packets.py b/scapy/contrib/automotive/v2x/packets.py index 2bb7818af82..d4c214aec54 100644 --- a/scapy/contrib/automotive/v2x/packets.py +++ b/scapy/contrib/automotive/v2x/packets.py @@ -22,6 +22,7 @@ ASN1F_SEQUENCE_OF, ASN1F_STRING, ASN1F_UTF8_STRING, + ASN1F_DEFAULT, ASN1F_optional, ) from scapy.asn1packet import ASN1_Packet @@ -69,6 +70,13 @@ class PathPoint(ASN1_Packet): ) +class PathHistory(ASN1_Packet): + ASN1_codec = ASN1_Codecs.PER + ASN1_root = ASN1F_SEQUENCE_OF( + "pathPoints", [], PathPoint, uper_min=0, uper_max=40 + ) + + class PtActivation(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( @@ -212,7 +220,7 @@ class EventPoint(ASN1_Packet): ASN1_codec = ASN1_Codecs.PER ASN1_root = ASN1F_SEQUENCE( ASN1F_PACKET("eventPosition", DeltaReferencePosition(), DeltaReferencePosition), - ASN1F_optional(ASN1F_INTEGER("eventDeltaTime", None, uper_min=1, uper_max=65535, oer_unsigned=True)), + ASN1F_optional(ASN1F_INTEGER("eventDeltaTime", None, uper_min=1, uper_max=65535, oer_unsigned=True, uper_extensible=True)), ASN1F_INTEGER("informationQuality", 0, uper_min=0, uper_max=7, oer_unsigned=True) ) @@ -337,8 +345,8 @@ class LocationContainer(ASN1_Packet): ASN1_root = ASN1F_SEQUENCE( ASN1F_optional(ASN1F_PACKET("eventSpeed", None, Speed)), ASN1F_optional(ASN1F_PACKET("eventPositionHeading", None, Heading)), - ASN1F_SEQUENCE_OF("traces", [], ASN1F_STRING, uper_min=1, uper_max=7), - ASN1F_optional(ASN1F_ENUMERATED("roadType", 0, {0: 'urban-NoStructuralSeparationToOppositeLanes', 1: 'urban-WithStructuralSeparationToOppositeLanes', 2: 'nonUrban-NoStructuralSeparationToOppositeLanes', 3: 'nonUrban-WithStructuralSeparationToOppositeLanes'})), uper_extensible=True + ASN1F_SEQUENCE_OF("traces", [], PathHistory, uper_min=1, uper_max=7), + ASN1F_optional(ASN1F_ENUMERATED("roadType", None, {0: 'urban-NoStructuralSeparationToOppositeLanes', 1: 'urban-WithStructuralSeparationToOppositeLanes', 2: 'nonUrban-NoStructuralSeparationToOppositeLanes', 3: 'nonUrban-WithStructuralSeparationToOppositeLanes'})), uper_extensible=True ) @@ -1029,7 +1037,10 @@ class ManagementContainer(ASN1_Packet): ASN1F_PACKET("eventPosition", ReferencePosition(), ReferencePosition), ASN1F_optional(ASN1F_ENUMERATED("relevanceDistance", 0, {0: 'lessThan50m', 1: 'lessThan100m', 2: 'lessThan200m', 3: 'lessThan500m', 4: 'lessThan1000m', 5: 'lessThan5km', 6: 'lessThan10km', 7: 'over10km'})), ASN1F_optional(ASN1F_ENUMERATED("relevanceTrafficDirection", 0, {0: 'allTrafficDirections', 1: 'upstreamTraffic', 2: 'downstreamTraffic', 3: 'oppositeTraffic'})), - ASN1F_INTEGER("validityDuration", 0, uper_min=0, uper_max=86400, oer_unsigned=True), + ASN1F_DEFAULT( + ASN1F_INTEGER("validityDuration", 600, uper_min=0, uper_max=86400, oer_unsigned=True), + 600, + ), ASN1F_optional(ASN1F_INTEGER("transmissionInterval", None, uper_min=1, uper_max=10000, oer_unsigned=True)), ASN1F_INTEGER("stationType", 0, uper_min=0, uper_max=255, oer_unsigned=True), uper_extensible=True ) @@ -1775,6 +1786,7 @@ class MAPEM(ASN1_Packet): "Altitude", "PosConfidenceEllipse", "PathPoint", + "PathHistory", "PtActivation", "CauseCode", "Curvature", diff --git a/test/contrib/automotive/v2x_packets.py b/test/contrib/automotive/v2x_packets.py index 335cd4aee81..f0c9519181d 100644 --- a/test/contrib/automotive/v2x_packets.py +++ b/test/contrib/automotive/v2x_packets.py @@ -9,7 +9,23 @@ from pathlib import Path from scapy.contrib.automotive.v2x import CAM, DENM, IVIM, MAPEM, SPATEM -from scapy.contrib.automotive.v2x.packets import ItsPduHeader +from scapy.contrib.automotive.v2x.packets import ( + ActionID, + Altitude, + CauseCode, + DecentralizedEnvironmentalNotificationMessage, + DeltaReferencePosition, + EventPoint, + Heading, + ItsPduHeader, + LocationContainer, + ManagementContainer, + PathHistory, + PosConfidenceEllipse, + ReferencePosition, + SituationContainer, + Speed, +) from scapy.packet import raw try: @@ -47,6 +63,176 @@ def _its_asn_files(): ] +def _its_asn_files_available(): + # type: () -> bool + return all(f.is_file() for f in _its_asn_files()) + + +# LF Edge InstantX UPER example (DENM inside G5/BTP payload): +# https://github.com/lf-edge/instantx/blob/main/docs/Encoding.md#examples +INSTANTX_DENM_PAYLOAD_HEX = ( + "11004c01204000800050010003db00009400000000000000adbed407f974b8e6f952f45d" + "000d000e7974b8e67952f45d000a00000000000007d200000201012fd537c78097ea9b81" + "ed9f2d8fa1a3c7cb63e868f2f19a1e6649cc65d17917900018b5010546001f3056c1c00" + "61000dff8480e018d841196eac3e4a01c780c518d3261453f0077e000" +) +# ITS DENM UPER (ItsPduHeader + DENM) after the BTP destination port header. +INSTANTX_DENM_ITS_HEX = ( + "0201012fd537c78097ea9b81ed9f2d8fa1a3c7cb63e868f2f19a1e6649cc65d179179" + "00018b5010546001f3056c1c0061000dff8480e018d841196eac3e4a01c780c518d326" + "1453f0077e000" +) + + +def _instantx_denm_its_bytes(): + # type: () -> bytes + payload = bytes.fromhex(INSTANTX_DENM_PAYLOAD_HEX) + # BTP-B header: destinationPort (2 bytes) + destinationPortInfo (2 bytes). + return payload[60:] + + +def _build_instantx_denm(): + # type: () -> DENM + return DENM( + header=ItsPduHeader( + protocolVersion=2, + messageID=1, + stationID=19911991, + ), + denm=DecentralizedEnvironmentalNotificationMessage( + management=ManagementContainer( + actionID=ActionID( + originatingStationID=19911991, + sequenceNumber=987, + ), + detectionTime=1071266991390, + referenceTime=1071266991390, + termination=None, + eventPosition=ReferencePosition( + latitude=-109791002, + longitude=-112004003, + positionConfidenceEllipse=PosConfidenceEllipse( + semiMajorConfidence=377, + semiMinorConfidence=377, + semiMajorOrientation=0, + ), + altitude=Altitude( + altitudeValue=1200, + altitudeConfidence=1, + ), + ), + relevanceDistance=0, + relevanceTrafficDirection=0, + validityDuration=86400, + transmissionInterval=500, + stationType=5, + ), + situation=SituationContainer( + informationQuality=3, + eventType=CauseCode(causeCode=14, subCauseCode=0), + linkedCause=CauseCode(causeCode=97, subCauseCode=0), + eventHistory=[ + EventPoint( + eventPosition=DeltaReferencePosition( + deltaLatitude=-123, + deltaLongitude=897, + deltaAltitude=20, + ), + eventDeltaTime=1706733817, + informationQuality=1, + ), + EventPoint( + eventPosition=DeltaReferencePosition( + deltaLatitude=456, + deltaLongitude=789, + deltaAltitude=10, + ), + informationQuality=2, + ), + ], + ), + location=LocationContainer( + eventSpeed=Speed(speedValue=1300, speedConfidence=127), + eventPositionHeading=Heading( + headingValue=14, + headingConfidence=127, + ), + traces=[PathHistory()], + ), + ), + ) + + +def _assert_instantx_denm_decoded(decoded): + # type: (dict) -> None + assert decoded["header"]["protocolVersion"] == 2 + assert decoded["header"]["messageID"] == 1 + assert decoded["header"]["stationID"] == 19911991 + mgmt = decoded["denm"]["management"] + assert mgmt["actionID"]["originatingStationID"] == 19911991 + assert mgmt["actionID"]["sequenceNumber"] == 987 + assert mgmt["detectionTime"] == 1071266991390 + assert mgmt["referenceTime"] == 1071266991390 + pos = mgmt["eventPosition"] + assert pos["latitude"] == -109791002 + assert pos["longitude"] == -112004003 + assert pos["positionConfidenceEllipse"]["semiMajorConfidence"] == 377 + assert pos["altitude"]["altitudeValue"] == 1200 + assert pos["altitude"]["altitudeConfidence"] == "alt-000-02" + assert mgmt["relevanceDistance"] == "lessThan50m" + assert mgmt["relevanceTrafficDirection"] == "allTrafficDirections" + assert mgmt["validityDuration"] == 86400 + assert mgmt["transmissionInterval"] == 500 + assert mgmt["stationType"] == 5 + situation = decoded["denm"]["situation"] + assert situation["informationQuality"] == 3 + assert situation["eventType"]["causeCode"] == 14 + assert situation["linkedCause"]["causeCode"] == 97 + history = situation["eventHistory"] + assert len(history) == 2 + assert history[0]["eventPosition"]["deltaLatitude"] == -123 + assert history[0]["eventDeltaTime"] == 1706733817 + assert history[1]["eventPosition"]["deltaLatitude"] == 456 + location = decoded["denm"]["location"] + assert location["eventSpeed"]["speedValue"] == 1300 + assert location["eventPositionHeading"]["headingValue"] == 14 + assert location["traces"] == [[]] + + +def check_instantx_denm_example_encode(): + # type: () -> None + got = raw(_build_instantx_denm()) + assert got == bytes.fromhex(INSTANTX_DENM_ITS_HEX) + + +def check_instantx_denm_example_scapy_decode(): + # type: () -> None + pkt = DENM(_instantx_denm_its_bytes()) + assert pkt.header.protocolVersion.val == 2 + assert pkt.header.messageID.val == 1 + assert pkt.header.stationID.val == 19911991 + mgmt = pkt.denm.management + assert mgmt.actionID.originatingStationID.val == 19911991 + assert mgmt.actionID.sequenceNumber.val == 987 + assert mgmt.detectionTime.val == 1071266991390 + assert mgmt.referenceTime.val == 1071266991390 + assert mgmt.termination is None + assert mgmt.eventPosition.latitude.val == -109791002 + assert mgmt.eventPosition.longitude.val == -112004003 + assert mgmt.validityDuration.val == 86400 + assert mgmt.transmissionInterval.val == 500 + assert mgmt.stationType.val == 5 + assert pkt.denm.situation.eventType.causeCode.val == 14 + assert pkt.denm.situation.linkedCause.causeCode.val == 97 + assert len(pkt.denm.situation.eventHistory) == 2 + assert pkt.denm.situation.eventHistory[0].eventDeltaTime.val == 1706733817 + assert pkt.denm.situation.eventHistory[0].eventPosition.deltaLatitude.val == -123 + assert pkt.denm.situation.eventHistory[1].eventPosition.deltaLatitude.val == 456 + assert pkt.denm.location.eventSpeed.speedValue.val == 1300 + assert len(pkt.denm.location.traces) == 1 + assert len(pkt.denm.location.traces[0].pathPoints) == 0 + + def check_its_import(): # type: () -> None assert CAM.__name__ == "CAM" @@ -98,3 +284,125 @@ def check_its_messages_asn1tools_decode(): data = raw(pkt) decoded = compiled.decode(name, data) assert decoded["header"]["stationID"] == 42 + + +def check_instantx_denm_example_payload(): + # type: () -> None + its = _instantx_denm_its_bytes() + assert its == bytes.fromhex(INSTANTX_DENM_ITS_HEX) + assert len(its) == 76 + + +def check_instantx_denm_example_build(): + # type: () -> None + pkt = _build_instantx_denm() + assert int(pkt.header.protocolVersion) == 2 + assert int(pkt.header.messageID) == 1 + assert int(pkt.header.stationID) == 19911991 + mgmt = pkt.denm.management + assert int(mgmt.actionID.sequenceNumber) == 987 + assert int(mgmt.detectionTime) == 1071266991390 + assert int(mgmt.eventPosition.latitude) == -109791002 + assert int(mgmt.eventPosition.altitude.altitudeValue) == 1200 + assert int(pkt.denm.situation.eventType.causeCode) == 14 + assert int(pkt.denm.situation.linkedCause.causeCode) == 97 + assert len(pkt.denm.situation.eventHistory) == 2 + assert int(pkt.denm.location.eventSpeed.speedValue) == 1300 + assert len(raw(pkt)) > 0 + + +def check_instantx_denm_example_roundtrip(): + # type: () -> None + built = _build_instantx_denm() + data = raw(built) + assert data == bytes.fromhex(INSTANTX_DENM_ITS_HEX) + decoded = DENM(data) + assert raw(decoded) == data + + +def check_its_containers_build(): + # type: () -> None + mgmt = ManagementContainer( + actionID=ActionID(originatingStationID=1, sequenceNumber=1), + detectionTime=1, + referenceTime=1, + eventPosition=ReferencePosition( + latitude=0, + longitude=0, + positionConfidenceEllipse=PosConfidenceEllipse( + semiMajorConfidence=0, + semiMinorConfidence=0, + semiMajorOrientation=0, + ), + altitude=Altitude(altitudeValue=0, altitudeConfidence=0), + ), + stationType=0, + ) + assert len(raw(mgmt)) > 0 + + situation = SituationContainer( + informationQuality=1, + eventType=CauseCode(causeCode=1, subCauseCode=0), + eventHistory=[ + EventPoint( + eventPosition=DeltaReferencePosition( + deltaLatitude=0, + deltaLongitude=0, + deltaAltitude=0, + ), + eventDeltaTime=1706733817, + informationQuality=1, + ), + ], + ) + assert len(raw(situation)) > 0 + assert SituationContainer(raw(situation)).eventHistory[0].eventDeltaTime.val == 1706733817 + + location = LocationContainer( + traces=[PathHistory()], + ) + assert len(raw(location)) > 0 + assert len(LocationContainer(raw(location)).traces) == 1 + + +def check_its_containers_dissect(): + # type: () -> None + situation = SituationContainer( + informationQuality=1, + eventType=CauseCode(causeCode=1, subCauseCode=0), + eventHistory=[ + EventPoint( + eventPosition=DeltaReferencePosition( + deltaLatitude=0, + deltaLongitude=0, + deltaAltitude=0, + ), + eventDeltaTime=1706733817, + informationQuality=1, + ), + ], + ) + data = raw(situation) + decoded = SituationContainer(data) + assert decoded.informationQuality.val == 1 + assert decoded.eventType.causeCode.val == 1 + assert len(decoded.eventHistory) == 1 + assert decoded.eventHistory[0].eventDeltaTime.val == 1706733817 + + location = LocationContainer(traces=[PathHistory()]) + data = raw(location) + decoded = LocationContainer(data) + assert len(decoded.traces) == 1 + assert len(decoded.traces[0].pathPoints) == 0 + + +def check_instantx_denm_example_asn1tools(): + # type: () -> None + if not HAS_ASN1TOOLS or not _its_asn_files_available(): + return + compiled = asn1tools.compile_files( + [str(f) for f in _its_asn_files()], + codec="uper", + ) + decoded = compiled.decode("DENM", _instantx_denm_its_bytes()) + _assert_instantx_denm_decoded(decoded) diff --git a/test/scapy/layers/asn1.uts b/test/scapy/layers/asn1.uts index 4f54e10b581..22ac6cbb474 100644 --- a/test/scapy/layers/asn1.uts +++ b/test/scapy/layers/asn1.uts @@ -461,6 +461,22 @@ __import__('test.contrib.automotive.v2x_packets', fromlist=['check_its_import']) __import__('test.contrib.automotive.v2x_packets', fromlist=['check_its_messages_build']).check_its_messages_build() = ITS messages asn1tools decode __import__('test.contrib.automotive.v2x_packets', fromlist=['check_its_messages_asn1tools_decode']).check_its_messages_asn1tools_decode() += InstantX DENM example payload +__import__('test.contrib.automotive.v2x_packets', fromlist=['check_instantx_denm_example_payload']).check_instantx_denm_example_payload() += InstantX DENM example build +__import__('test.contrib.automotive.v2x_packets', fromlist=['check_instantx_denm_example_build']).check_instantx_denm_example_build() += InstantX DENM example asn1tools +__import__('test.contrib.automotive.v2x_packets', fromlist=['check_instantx_denm_example_asn1tools']).check_instantx_denm_example_asn1tools() += InstantX DENM example encode +__import__('test.contrib.automotive.v2x_packets', fromlist=['check_instantx_denm_example_encode']).check_instantx_denm_example_encode() += InstantX DENM example scapy decode +__import__('test.contrib.automotive.v2x_packets', fromlist=['check_instantx_denm_example_scapy_decode']).check_instantx_denm_example_scapy_decode() += InstantX DENM example roundtrip +__import__('test.contrib.automotive.v2x_packets', fromlist=['check_instantx_denm_example_roundtrip']).check_instantx_denm_example_roundtrip() += ITS containers build +__import__('test.contrib.automotive.v2x_packets', fromlist=['check_its_containers_build']).check_its_containers_build() += ITS containers dissect +__import__('test.contrib.automotive.v2x_packets', fromlist=['check_its_containers_dissect']).check_its_containers_dissect() + ASN.1 UPER fuzzing = UPER fuzz encode diff --git a/tox.ini b/tox.ini index e29e5fbc940..a7a6b849486 100644 --- a/tox.ini +++ b/tox.ini @@ -193,5 +193,7 @@ per-file-ignores = scapy/libs/winpcapy.py:F405,F403,E501 scapy/libs/manuf.py:E501 scapy/tools/UTscapy.py:E501 + scapy/tools/generate_its_asn1.py:E501 exclude = scapy/libs/ethertypes.py, - scapy/layers/msrpce/raw/* + scapy/layers/msrpce/raw/*, + scapy/contrib/automotive/v2x/packets.py From bca5cabc21a49f0895d08d20f6627e9d7e6c5300 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 3 Jul 2026 08:29:07 +0200 Subject: [PATCH 14/15] more tests AI-Assisted: yes (Cursor AI) --- test/contrib/automotive/v2x_packets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/contrib/automotive/v2x_packets.py b/test/contrib/automotive/v2x_packets.py index f0c9519181d..04b767e2f56 100644 --- a/test/contrib/automotive/v2x_packets.py +++ b/test/contrib/automotive/v2x_packets.py @@ -262,7 +262,7 @@ def check_its_messages_build(): def check_its_messages_asn1tools_decode(): # type: () -> None - if not HAS_ASN1TOOLS: + if not HAS_ASN1TOOLS or not _its_asn_files_available(): return compiled = asn1tools.compile_files( [str(f) for f in _its_asn_files()], From 8e99ddbfe7ddab54b41ceaa186834f9d993ed5ab Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Fri, 3 Jul 2026 10:43:34 +0200 Subject: [PATCH 15/15] fix tests AI-Assisted: yes (Cursor AI) --- scapy/contrib/automotive/v2x/__init__.py | 1 + scapy/contrib/automotive/v2x/packets.py | 2 ++ scapy/tools/generate_its_asn1.py | 2 ++ 3 files changed, 5 insertions(+) diff --git a/scapy/contrib/automotive/v2x/__init__.py b/scapy/contrib/automotive/v2x/__init__.py index b8a561278cf..b51f8e983f2 100644 --- a/scapy/contrib/automotive/v2x/__init__.py +++ b/scapy/contrib/automotive/v2x/__init__.py @@ -2,6 +2,7 @@ # This file is part of Scapy # See https://scapy.net/ for more information +# scapy.contrib.description = ETSI ITS V2X ASN.1 messages (UPER) # scapy.contrib.status = library """ diff --git a/scapy/contrib/automotive/v2x/packets.py b/scapy/contrib/automotive/v2x/packets.py index d4c214aec54..74840fd9494 100644 --- a/scapy/contrib/automotive/v2x/packets.py +++ b/scapy/contrib/automotive/v2x/packets.py @@ -2,6 +2,8 @@ # This file is part of Scapy # See https://scapy.net/ for more information # AUTO-GENERATED by scapy/tools/generate_its_asn1.py - DO NOT EDIT + +# scapy.contrib.status = skip """ ETSI ITS ASN.1 packets (UPER): CAM, DENM, IVIM, SPATEM, MAPEM. """ diff --git a/scapy/tools/generate_its_asn1.py b/scapy/tools/generate_its_asn1.py index 69f496800d7..fb87a0680e1 100644 --- a/scapy/tools/generate_its_asn1.py +++ b/scapy/tools/generate_its_asn1.py @@ -814,6 +814,8 @@ def generate(self) -> str: "# This file is part of Scapy", "# See https://scapy.net/ for more information", "# AUTO-GENERATED by scapy/tools/generate_its_asn1.py - DO NOT EDIT", + "", + "# scapy.contrib.status = skip", '"""', "ETSI ITS ASN.1 packets (UPER): CAM, DENM, IVIM, SPATEM, MAPEM.", '"""',