X-Git-Url: http://www.git.cypherpunks.ru/?a=blobdiff_plain;f=pyderasn.py;h=50faa0be870ced74ab9d33afb4ac0f4008846421;hb=97fe8b8eae995499566e3ae26e2e941e633997bc;hp=b668507f3a46f9cccdf8d1cfbe3f97ae1cde9826;hpb=be964cef343716ff80679f2bc828427bc07c5f02;p=pyderasn.git diff --git a/pyderasn.py b/pyderasn.py index b668507..50faa0b 100755 --- a/pyderasn.py +++ b/pyderasn.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # coding: utf-8 # PyDERASN -- Python ASN.1 DER/BER codec with abstract structures -# Copyright (C) 2017-2018 Sergey Matveev +# Copyright (C) 2017-2019 Sergey Matveev # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as @@ -189,9 +189,16 @@ use following properties: Pay attention that those values do **not** include anything related to explicit tag. If you want to know information about it, then use: -``expled`` (to know if explicit tag is set), ``expl_offset`` (it is -lesser than ``offset``), ``expl_tlen``, ``expl_llen``, ``expl_vlen`` -(that actually equals to ordinary ``tlvlen``). + +* ``expled`` -- to know if explicit tag is set +* ``expl_offset`` (it is lesser than ``offset``) +* ``expl_tlen``, +* ``expl_llen`` +* ``expl_vlen`` (that actually equals to ordinary ``tlvlen``) +* ``fulloffset`` -- it equals to ``expl_offset`` if explicit tag is set, + ``offset`` otherwise +* ``fulllen`` -- it equals to ``expl_len`` if explicit tag is set, + ``tlvlen`` otherwise When error occurs, :py:exc:`pyderasn.DecodeError` is raised. @@ -206,9 +213,11 @@ decoding process. Currently available context options: +* :ref:`allow_default_values ` +* :ref:`allow_expl_oob ` +* :ref:`allow_unordered_set ` * :ref:`bered ` * :ref:`defines_by_path ` -* :ref:`strict_default_existence ` .. _pprinting: @@ -267,7 +276,7 @@ You can specify multiple fields, that will be autodecoded -- that is why ``defines`` kwarg is a sequence. You can specify defined field relatively or absolutely to current decode path. For example ``defines`` for AlgorithmIdentifier of X.509's -``tbsCertificate.subjectPublicKeyInfo.algorithm.algorithm``:: +``tbsCertificate:subjectPublicKeyInfo:algorithm:algorithm``:: ( (("parameters",), { @@ -295,8 +304,7 @@ Following types can be automatically decoded (DEFINED BY): When any of those fields is automatically decoded, then ``.defined`` attribute contains ``(OID, value)`` tuple. ``OID`` tells by which OID it was defined, ``value`` contains corresponding decoded value. For example -above, ``content_info["content"].defined == (id_signedData, -signed_data)``. +above, ``content_info["content"].defined == (id_signedData, signed_data)``. .. _defines_by_path_ctx: @@ -374,19 +382,42 @@ DER. But you can optionally enable BER decoding with setting ``bered`` :ref:`context ` argument to True. Indefinite lengths and constructed primitive types should be parsed successfully. -* If object is encoded in BER form (not the DER one), then ``bered`` +* If object is encoded in BER form (not the DER one), then ``ber_encoded`` attribute is set to True. Only ``BOOLEAN``, ``BIT STRING``, ``OCTET - STRING`` can contain it. + STRING``, ``OBJECT IDENTIFIER``, ``SEQUENCE``, ``SET``, ``SET OF`` + can contain it. * If object has an indefinite length encoding, then its ``lenindef`` attribute is set to True. Only ``BIT STRING``, ``OCTET STRING``, ``SEQUENCE``, ``SET``, ``SEQUENCE OF``, ``SET OF``, ``ANY`` can contain it. * If object has an indefinite length encoded explicit tag, then ``expl_lenindef`` is set to True. +* If object has either any of BER-related encoding (explicit tag + indefinite length, object's indefinite length, BER-encoding) or any + underlying component has that kind of encoding, then ``bered`` + attribute is set to True. For example SignedData CMS can have + ``ContentInfo:content:signerInfos:*`` ``bered`` value set to True, but + ``ContentInfo:content:signerInfos:*:signedAttrs`` won't. EOC (end-of-contents) token's length is taken in advance in object's value length. +.. _allow_expl_oob_ctx: + +Allow explicit tag out-of-bound +------------------------------- + +Invalid BER encoding could contain ``EXPLICIT`` tag containing more than +one value, more than one object. If you set ``allow_expl_oob`` context +option to True, then no error will be raised and that invalid encoding +will be silently further processed. But pay attention that offsets and +lengths will be invalid in that case. + +.. warning:: + + This option should be used only for skipping some decode errors, just + to see the decoded structure somehow. + Primitive types --------------- @@ -485,6 +516,7 @@ Various ------- .. autofunction:: pyderasn.abs_decode_path +.. autofunction:: pyderasn.colonize_hex .. autofunction:: pyderasn.hexenc .. autofunction:: pyderasn.hexdec .. autofunction:: pyderasn.tag_encode @@ -509,9 +541,11 @@ from codecs import getdecoder from codecs import getencoder from collections import namedtuple from collections import OrderedDict +from copy import copy from datetime import datetime from math import ceil from os import environ +from string import ascii_letters from string import digits from six import add_metaclass @@ -521,15 +555,18 @@ from six import indexbytes from six import int2byte from six import integer_types from six import iterbytes +from six import iteritems +from six import itervalues from six import PY2 from six import string_types from six import text_type +from six import unichr as six_unichr from six.moves import xrange as six_xrange try: from termcolor import colored -except ImportError: +except ImportError: # pragma: no cover def colored(what, *args): return what @@ -604,13 +641,18 @@ TagClassReprs = { EOC = b"\x00\x00" EOC_LEN = len(EOC) LENINDEF = b"\x80" # length indefinite mark +LENINDEF_PP_CHAR = "I" if PY2 else "∞" ######################################################################## # Errors ######################################################################## -class DecodeError(Exception): +class ASN1Error(ValueError): + pass + + +class DecodeError(ASN1Error): def __init__(self, msg="", klass=None, decode_path=(), offset=0): """ :param str msg: reason of decode failing @@ -633,7 +675,7 @@ class DecodeError(Exception): c for c in ( "" if self.klass is None else self.klass.__name__, ( - ("(%s)" % ".".join(str(dp) for dp in self.decode_path)) + ("(%s)" % ":".join(str(dp) for dp in self.decode_path)) if len(self.decode_path) > 0 else "" ), ("(at %d)" % self.offset) if self.offset > 0 else "", @@ -665,7 +707,7 @@ class InvalidOID(DecodeError): pass -class ObjUnknown(ValueError): +class ObjUnknown(ASN1Error): def __init__(self, name): super(ObjUnknown, self).__init__() self.name = name @@ -677,7 +719,7 @@ class ObjUnknown(ValueError): return "%s(%s)" % (self.__class__.__name__, self) -class ObjNotReady(ValueError): +class ObjNotReady(ASN1Error): def __init__(self, name): super(ObjNotReady, self).__init__() self.name = name @@ -689,7 +731,7 @@ class ObjNotReady(ValueError): return "%s(%s)" % (self.__class__.__name__, self) -class InvalidValueType(ValueError): +class InvalidValueType(ASN1Error): def __init__(self, expected_types): super(InvalidValueType, self).__init__() self.expected_types = expected_types @@ -703,7 +745,7 @@ class InvalidValueType(ValueError): return "%s(%s)" % (self.__class__.__name__, self) -class BoundsError(ValueError): +class BoundsError(ASN1Error): def __init__(self, bound_min, value, bound_max): super(BoundsError, self).__init__() self.bound_min = bound_min @@ -897,7 +939,7 @@ class Obj(object): "vlen", "expl_lenindef", "lenindef", - "bered", + "ber_encoded", ) def __init__( @@ -919,7 +961,7 @@ class Obj(object): self.default = None self.expl_lenindef = False self.lenindef = False - self.bered = False + self.ber_encoded = False @property def ready(self): # pragma: no cover @@ -931,6 +973,12 @@ class Obj(object): if not self.ready: raise ObjNotReady(self.__class__.__name__) + @property + def bered(self): + """Is either object or any elements inside is BER encoded? + """ + return self.expl_lenindef or self.lenindef or self.ber_encoded + @property def decoded(self): """Is object decoded? @@ -985,6 +1033,7 @@ class Obj(object): decode_path=(), ctx=None, tag_only=False, + _ctx_immutable=True, ): """Decode the data @@ -992,14 +1041,17 @@ class Obj(object): :param int offset: initial data's offset :param bool leavemm: do we need to leave memoryview of remaining data as is, or convert it to bytes otherwise - :param ctx: optional :ref:`context ` governing decoding process. + :param ctx: optional :ref:`context ` governing decoding process :param tag_only: decode only the tag, without length and contents (used only in Choice and Set structures, trying to determine if tag satisfies the scheme) + :param _ctx_immutable: do we need to copy ``ctx`` before using it :returns: (Obj, remaining data) """ if ctx is None: ctx = {} + elif _ctx_immutable: + ctx = copy(ctx) tlv = memoryview(data) if self._expl is None: result = self._decode( @@ -1047,7 +1099,7 @@ class Obj(object): ctx=ctx, tag_only=tag_only, ) - if tag_only: + if tag_only: # pragma: no cover return obj, tail = result eoc_expected, tail = tail[:EOC_LEN], tail[EOC_LEN:] @@ -1082,9 +1134,16 @@ class Obj(object): ctx=ctx, tag_only=tag_only, ) - if tag_only: + if tag_only: # pragma: no cover return obj, tail = result + if obj.tlvlen < l and not ctx.get("allow_expl_oob", False): + raise DecodeError( + "explicit tag out-of-bound, longer than data", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) return obj, (tail if leavemm else tail.tobytes()) @property @@ -1117,6 +1176,46 @@ class Obj(object): def expl_tlvlen(self): return self.expl_tlen + self.expl_llen + self.expl_vlen + @property + def fulloffset(self): + return self.expl_offset if self.expled else self.offset + + @property + def fulllen(self): + return self.expl_tlvlen if self.expled else self.tlvlen + + def pps_lenindef(self, decode_path): + if self.lenindef and not ( + getattr(self, "defined", None) is not None and + self.defined[1].lenindef + ): + yield _pp( + asn1_type_name="EOC", + obj_name="", + decode_path=decode_path, + offset=( + self.offset + self.tlvlen - + (EOC_LEN * 2 if self.expl_lenindef else EOC_LEN) + ), + tlen=1, + llen=1, + vlen=0, + ber_encoded=True, + bered=True, + ) + if self.expl_lenindef: + yield _pp( + asn1_type_name="EOC", + obj_name="EXPLICIT", + decode_path=decode_path, + offset=self.expl_offset + self.expl_tlvlen - EOC_LEN, + tlen=1, + llen=1, + vlen=0, + ber_encoded=True, + bered=True, + ) + class DecodePathDefBy(object): """DEFINED BY representation inside decode path @@ -1146,6 +1245,7 @@ class DecodePathDefBy(object): ######################################################################## PP = namedtuple("PP", ( + "obj", "asn1_type_name", "obj_name", "decode_path", @@ -1165,11 +1265,13 @@ PP = namedtuple("PP", ( "expl_vlen", "expl_lenindef", "lenindef", + "ber_encoded", "bered", )) def _pp( + obj=None, asn1_type_name="unknown", obj_name="unknown", decode_path=(), @@ -1189,9 +1291,11 @@ def _pp( expl_vlen=None, expl_lenindef=False, lenindef=False, + ber_encoded=False, bered=False, ): return PP( + obj, asn1_type_name, obj_name, decode_path, @@ -1211,48 +1315,57 @@ def _pp( expl_vlen, expl_lenindef, lenindef, + ber_encoded, bered, ) -def _colorize(what, colour, with_colours, attrs=("bold",)): +def _colourize(what, colour, with_colours, attrs=("bold",)): return colored(what, colour, attrs=attrs) if with_colours else what +def colonize_hex(hexed): + """Separate hexadecimal string with colons + """ + return ":".join(hexed[i:i + 2] for i in six_xrange(0, len(hexed), 2)) + + def pp_console_row( pp, oids=None, with_offsets=False, with_blob=True, with_colours=False, + with_decode_path=False, + decode_path_len_decrease=0, ): cols = [] if with_offsets: - col = "%5d%s" % ( + col = "%5d%s%s" % ( pp.offset, ( " " if pp.expl_offset is None else ("-%d" % (pp.offset - pp.expl_offset)) ), + LENINDEF_PP_CHAR if pp.expl_lenindef else " ", ) - cols.append(_colorize(col, "red", with_colours, ())) - col = "[%d,%d,%4d]" % (pp.tlen, pp.llen, pp.vlen) - col = _colorize(col, "green", with_colours, ()) - ber_deoffset = 0 - if pp.expl_lenindef: - ber_deoffset += 2 - if pp.lenindef: - ber_deoffset += 2 - col += ( - " " if ber_deoffset == 0 else - _colorize(("-%d" % ber_deoffset), "red", with_colours) + col = _colourize(col, "red", with_colours, ()) + col += _colourize("B", "red", with_colours) if pp.bered else " " + cols.append(col) + col = "[%d,%d,%4d]%s" % ( + pp.tlen, + pp.llen, + pp.vlen, + LENINDEF_PP_CHAR if pp.lenindef else " " ) + col = _colourize(col, "green", with_colours, ()) cols.append(col) - if len(pp.decode_path) > 0: - cols.append(" ." * (len(pp.decode_path))) + decode_path_len = len(pp.decode_path) - decode_path_len_decrease + if decode_path_len > 0: + cols.append(" ." * decode_path_len) ent = pp.decode_path[-1] if isinstance(ent, DecodePathDefBy): - cols.append(_colorize("DEFINED BY", "red", with_colours, ("reverse",))) + cols.append(_colourize("DEFINED BY", "red", with_colours, ("reverse",))) value = str(ent.defined_by) if ( oids is not None and @@ -1260,61 +1373,82 @@ def pp_console_row( ObjectIdentifier.asn1_type_name and value in oids ): - cols.append(_colorize("%s:" % oids[value], "green", with_colours)) + cols.append(_colourize("%s:" % oids[value], "green", with_colours)) else: - cols.append(_colorize("%s:" % value, "white", with_colours, ("reverse",))) + cols.append(_colourize("%s:" % value, "white", with_colours, ("reverse",))) else: - cols.append(_colorize("%s:" % ent, "yellow", with_colours, ("reverse",))) + cols.append(_colourize("%s:" % ent, "yellow", with_colours, ("reverse",))) if pp.expl is not None: klass, _, num = pp.expl col = "[%s%d] EXPLICIT" % (TagClassReprs[klass], num) - cols.append(_colorize(col, "blue", with_colours)) + cols.append(_colourize(col, "blue", with_colours)) if pp.impl is not None: klass, _, num = pp.impl col = "[%s%d]" % (TagClassReprs[klass], num) - cols.append(_colorize(col, "blue", with_colours)) + cols.append(_colourize(col, "blue", with_colours)) if pp.asn1_type_name.replace(" ", "") != pp.obj_name.upper(): - cols.append(_colorize(pp.obj_name, "magenta", with_colours)) - if pp.bered: - cols.append(_colorize("BER", "red", with_colours)) - cols.append(_colorize(pp.asn1_type_name, "cyan", with_colours)) + cols.append(_colourize(pp.obj_name, "magenta", with_colours)) + if pp.ber_encoded: + cols.append(_colourize("BER", "red", with_colours)) + cols.append(_colourize(pp.asn1_type_name, "cyan", with_colours)) if pp.value is not None: value = pp.value - cols.append(_colorize(value, "white", with_colours, ("reverse",))) + cols.append(_colourize(value, "white", with_colours, ("reverse",))) if ( oids is not None and pp.asn1_type_name == ObjectIdentifier.asn1_type_name and value in oids ): - cols.append(_colorize("(%s)" % oids[value], "green", with_colours)) + cols.append(_colourize("(%s)" % oids[value], "green", with_colours)) + if pp.asn1_type_name == Integer.asn1_type_name: + hex_repr = hex(int(pp.obj._value))[2:].upper() + if len(hex_repr) % 2 != 0: + hex_repr = "0" + hex_repr + cols.append(_colourize( + "(%s)" % colonize_hex(hex_repr), + "green", + with_colours, + )) if with_blob: if isinstance(pp.blob, binary_type): cols.append(hexenc(pp.blob)) elif isinstance(pp.blob, tuple): cols.append(", ".join(pp.blob)) if pp.optional: - cols.append(_colorize("OPTIONAL", "red", with_colours)) + cols.append(_colourize("OPTIONAL", "red", with_colours)) if pp.default: - cols.append(_colorize("DEFAULT", "red", with_colours)) + cols.append(_colourize("DEFAULT", "red", with_colours)) + if with_decode_path: + cols.append(_colourize( + "[%s]" % ":".join(str(p) for p in pp.decode_path), + "grey", + with_colours, + )) return " ".join(cols) -def pp_console_blob(pp): - cols = [" " * len("XXXXXYY [X,X,XXXX]YY")] - if len(pp.decode_path) > 0: - cols.append(" ." * (len(pp.decode_path) + 1)) +def pp_console_blob(pp, decode_path_len_decrease=0): + cols = [" " * len("XXXXXYYZZ [X,X,XXXX]Z")] + decode_path_len = len(pp.decode_path) - decode_path_len_decrease + if decode_path_len > 0: + cols.append(" ." * (decode_path_len + 1)) if isinstance(pp.blob, binary_type): blob = hexenc(pp.blob).upper() - for i in range(0, len(blob), 32): + for i in six_xrange(0, len(blob), 32): chunk = blob[i:i + 32] - yield " ".join(cols + [":".join( - chunk[j:j + 2] for j in range(0, len(chunk), 2) - )]) + yield " ".join(cols + [colonize_hex(chunk)]) elif isinstance(pp.blob, tuple): yield " ".join(cols + [", ".join(pp.blob)]) -def pprint(obj, oids=None, big_blobs=False, with_colours=False): +def pprint( + obj, + oids=None, + big_blobs=False, + with_colours=False, + with_decode_path=False, + decode_path_only=(), +): """Pretty print object :param Obj obj: object you want to pretty print @@ -1325,10 +1459,19 @@ def pprint(obj, oids=None, big_blobs=False, with_colours=False): lines :param with_colours: colourize output, if ``termcolor`` library is available + :param with_decode_path: print decode path + :param decode_path_only: print only that specified decode path """ def _pprint_pps(pps): for pp in pps: if hasattr(pp, "_fields"): + if ( + decode_path_only != () and + tuple( + str(p) for p in pp.decode_path[:len(decode_path_only)] + ) != decode_path_only + ): + continue if big_blobs: yield pp_console_row( pp, @@ -1336,8 +1479,13 @@ def pprint(obj, oids=None, big_blobs=False, with_colours=False): with_offsets=True, with_blob=False, with_colours=with_colours, + with_decode_path=with_decode_path, + decode_path_len_decrease=len(decode_path_only), ) - for row in pp_console_blob(pp): + for row in pp_console_blob( + pp, + decode_path_len_decrease=len(decode_path_only), + ): yield row else: yield pp_console_row( @@ -1346,6 +1494,8 @@ def pprint(obj, oids=None, big_blobs=False, with_colours=False): with_offsets=True, with_blob=True, with_colours=with_colours, + with_decode_path=with_decode_path, + decode_path_len_decrease=len(decode_path_only), ) else: for row in _pprint_pps(pp): @@ -1401,10 +1551,10 @@ class Boolean(Obj): self._value = default def _value_sanitize(self, value): - if issubclass(value.__class__, Boolean): - return value._value if isinstance(value, bool): return value + if issubclass(value.__class__, Boolean): + return value._value raise InvalidValueType((self.__class__, bool)) @property @@ -1421,6 +1571,9 @@ class Boolean(Obj): obj.offset = self.offset obj.llen = self.llen obj.vlen = self.vlen + obj.expl_lenindef = self.expl_lenindef + obj.lenindef = self.lenindef + obj.ber_encoded = self.ber_encoded return obj def __nonzero__(self): @@ -1508,14 +1661,14 @@ class Boolean(Obj): offset=offset, ) first_octet = byte2int(v) - bered = False + ber_encoded = False if first_octet == 0: value = False elif first_octet == 0xFF: value = True elif ctx.get("bered", False): value = True - bered = True + ber_encoded = True else: raise DecodeError( "unacceptable Boolean value", @@ -1531,7 +1684,7 @@ class Boolean(Obj): optional=self.optional, _decoded=(offset, 1, 1), ) - obj.bered = bered + obj.ber_encoded = ber_encoded return obj, v[1:] def __repr__(self): @@ -1539,6 +1692,7 @@ class Boolean(Obj): def pps(self, decode_path=()): yield _pp( + obj=self, asn1_type_name=self.asn1_type_name, obj_name=self.__class__.__name__, decode_path=decode_path, @@ -1556,8 +1710,11 @@ class Boolean(Obj): expl_llen=self.expl_llen if self.expled else None, expl_vlen=self.expl_vlen if self.expled else None, expl_lenindef=self.expl_lenindef, + ber_encoded=self.ber_encoded, bered=self.bered, ) + for pp in self.pps_lenindef(decode_path): + yield pp class Integer(Obj): @@ -1643,10 +1800,10 @@ class Integer(Obj): self._value = default def _value_sanitize(self, value): - if issubclass(value.__class__, Integer): - value = value._value - elif isinstance(value, integer_types): + if isinstance(value, integer_types): pass + elif issubclass(value.__class__, Integer): + value = value._value elif isinstance(value, str): value = self.specs.get(value) if value is None: @@ -1673,6 +1830,9 @@ class Integer(Obj): obj.offset = self.offset obj.llen = self.llen obj.vlen = self.vlen + obj.expl_lenindef = self.expl_lenindef + obj.lenindef = self.lenindef + obj.ber_encoded = self.ber_encoded return obj def __int__(self): @@ -1703,7 +1863,7 @@ class Integer(Obj): @property def named(self): - for name, value in self.specs.items(): + for name, value in iteritems(self.specs): if value == self._value: return name @@ -1863,6 +2023,7 @@ class Integer(Obj): def pps(self, decode_path=()): yield _pp( + obj=self, asn1_type_name=self.asn1_type_name, obj_name=self.__class__.__name__, decode_path=decode_path, @@ -1880,7 +2041,13 @@ class Integer(Obj): expl_llen=self.expl_llen if self.expled else None, expl_vlen=self.expl_vlen if self.expled else None, expl_lenindef=self.expl_lenindef, + bered=self.bered, ) + for pp in self.pps_lenindef(decode_path): + yield pp + + +SET01 = frozenset(("0", "1")) class BitString(Obj): @@ -1988,8 +2155,6 @@ class BitString(Obj): return bit_len, bytes(octets) def _value_sanitize(self, value): - if issubclass(value.__class__, BitString): - return value._value if isinstance(value, (string_types, binary_type)): if ( isinstance(value, string_types) and @@ -1997,7 +2162,7 @@ class BitString(Obj): ): if value.endswith("'B"): value = value[1:-2] - if not set(value) <= set(("0", "1")): + if not frozenset(value) <= SET01: raise ValueError("B's coding contains unacceptable chars") return self._bits2octets(value) elif value.endswith("'H"): @@ -2025,11 +2190,13 @@ class BitString(Obj): bits.append(bit) if len(bits) == 0: return self._bits2octets("") - bits = set(bits) + bits = frozenset(bits) return self._bits2octets("".join( ("1" if bit in bits else "0") for bit in six_xrange(max(bits) + 1) )) + if issubclass(value.__class__, BitString): + return value._value raise InvalidValueType((self.__class__, binary_type, string_types)) @property @@ -2049,6 +2216,9 @@ class BitString(Obj): obj.offset = self.offset obj.llen = self.llen obj.vlen = self.vlen + obj.expl_lenindef = self.expl_lenindef + obj.lenindef = self.lenindef + obj.ber_encoded = self.ber_encoded return obj def __iter__(self): @@ -2078,7 +2248,7 @@ class BitString(Obj): @property def named(self): - return [name for name, bit in self.specs.items() if self[bit]] + return [name for name, bit in iteritems(self.specs) if self[bit]] def __call__( self, @@ -2192,7 +2362,7 @@ class BitString(Obj): offset=offset, ) if t == self.tag: - if tag_only: + if tag_only: # pragma: no cover return return self._decode_chunk(lv, offset, decode_path, ctx) if t == self.tag_constructed: @@ -2203,7 +2373,7 @@ class BitString(Obj): decode_path=decode_path, offset=offset, ) - if tag_only: + if tag_only: # pragma: no cover return lenindef = False try: @@ -2257,6 +2427,7 @@ class BitString(Obj): decode_path=sub_decode_path, leavemm=True, ctx=ctx, + _ctx_immutable=False, ) except TagMismatch: raise DecodeError( @@ -2301,7 +2472,7 @@ class BitString(Obj): _decoded=(offset, llen, vlen + (EOC_LEN if lenindef else 0)), ) obj.lenindef = lenindef - obj.bered = True + obj.ber_encoded = True return obj, (v[EOC_LEN:] if lenindef else v) raise TagMismatch( klass=self.__class__, @@ -2321,6 +2492,7 @@ class BitString(Obj): if len(self.specs) > 0: blob = tuple(self.named) yield _pp( + obj=self, asn1_type_name=self.asn1_type_name, obj_name=self.__class__.__name__, decode_path=decode_path, @@ -2340,6 +2512,7 @@ class BitString(Obj): expl_vlen=self.expl_vlen if self.expled else None, expl_lenindef=self.expl_lenindef, lenindef=self.lenindef, + ber_encoded=self.ber_encoded, bered=self.bered, ) defined_by, defined = self.defined or (None, None) @@ -2347,6 +2520,8 @@ class BitString(Obj): yield defined.pps( decode_path=decode_path + (DecodePathDefBy(defined_by),) ) + for pp in self.pps_lenindef(decode_path): + yield pp class OctetString(Obj): @@ -2430,10 +2605,10 @@ class OctetString(Obj): ) def _value_sanitize(self, value): - if issubclass(value.__class__, OctetString): - value = value._value - elif isinstance(value, binary_type): + if isinstance(value, binary_type): pass + elif issubclass(value.__class__, OctetString): + value = value._value else: raise InvalidValueType((self.__class__, bytes)) if not self._bound_min <= len(value) <= self._bound_max: @@ -2456,6 +2631,9 @@ class OctetString(Obj): obj.offset = self.offset obj.llen = self.llen obj.vlen = self.vlen + obj.expl_lenindef = self.expl_lenindef + obj.lenindef = self.lenindef + obj.ber_encoded = self.ber_encoded return obj def __bytes__(self): @@ -2618,6 +2796,7 @@ class OctetString(Obj): decode_path=sub_decode_path, leavemm=True, ctx=ctx, + _ctx_immutable=False, ) except TagMismatch: raise DecodeError( @@ -2655,7 +2834,7 @@ class OctetString(Obj): offset=offset, ) obj.lenindef = lenindef - obj.bered = True + obj.ber_encoded = True return obj, (v[EOC_LEN:] if lenindef else v) raise TagMismatch( klass=self.__class__, @@ -2668,6 +2847,7 @@ class OctetString(Obj): def pps(self, decode_path=()): yield _pp( + obj=self, asn1_type_name=self.asn1_type_name, obj_name=self.__class__.__name__, decode_path=decode_path, @@ -2687,6 +2867,7 @@ class OctetString(Obj): expl_vlen=self.expl_vlen if self.expled else None, expl_lenindef=self.expl_lenindef, lenindef=self.lenindef, + ber_encoded=self.ber_encoded, bered=self.bered, ) defined_by, defined = self.defined or (None, None) @@ -2694,6 +2875,8 @@ class OctetString(Obj): yield defined.pps( decode_path=decode_path + (DecodePathDefBy(defined_by),) ) + for pp in self.pps_lenindef(decode_path): + yield pp class Null(Obj): @@ -2737,6 +2920,9 @@ class Null(Obj): obj.offset = self.offset obj.llen = self.llen obj.vlen = self.vlen + obj.expl_lenindef = self.expl_lenindef + obj.lenindef = self.lenindef + obj.ber_encoded = self.ber_encoded return obj def __eq__(self, their): @@ -2779,7 +2965,7 @@ class Null(Obj): decode_path=decode_path, offset=offset, ) - if tag_only: + if tag_only: # pragma: no cover return try: l, _, v = len_decode(lv) @@ -2810,6 +2996,7 @@ class Null(Obj): def pps(self, decode_path=()): yield _pp( + obj=self, asn1_type_name=self.asn1_type_name, obj_name=self.__class__.__name__, decode_path=decode_path, @@ -2825,7 +3012,10 @@ class Null(Obj): expl_llen=self.expl_llen if self.expled else None, expl_vlen=self.expl_vlen if self.expled else None, expl_lenindef=self.expl_lenindef, + bered=self.bered, ) + for pp in self.pps_lenindef(decode_path): + yield pp class ObjectIdentifier(Obj): @@ -2943,6 +3133,9 @@ class ObjectIdentifier(Obj): obj.offset = self.offset obj.llen = self.llen obj.vlen = self.vlen + obj.expl_lenindef = self.expl_lenindef + obj.lenindef = self.lenindef + obj.ber_encoded = self.ber_encoded return obj def __iter__(self): @@ -3027,7 +3220,7 @@ class ObjectIdentifier(Obj): decode_path=decode_path, offset=offset, ) - if tag_only: + if tag_only: # pragma: no cover return try: l, llen, v = len_decode(lv) @@ -3054,11 +3247,17 @@ class ObjectIdentifier(Obj): ) v, tail = v[:l], v[l:] arcs = [] + ber_encoded = False while len(v) > 0: i = 0 arc = 0 while True: octet = indexbytes(v, i) + if i == 0 and octet == 0x80: + if ctx.get("bered", False): + ber_encoded = True + else: + raise DecodeError("non normalized arc encoding") arc = (arc << 7) | (octet & 0x7F) if octet & 0x80 == 0: arcs.append(arc) @@ -3090,6 +3289,8 @@ class ObjectIdentifier(Obj): optional=self.optional, _decoded=(offset, llen, l), ) + if ber_encoded: + obj.ber_encoded = True return obj, tail def __repr__(self): @@ -3097,6 +3298,7 @@ class ObjectIdentifier(Obj): def pps(self, decode_path=()): yield _pp( + obj=self, asn1_type_name=self.asn1_type_name, obj_name=self.__class__.__name__, decode_path=decode_path, @@ -3114,7 +3316,11 @@ class ObjectIdentifier(Obj): expl_llen=self.expl_llen if self.expled else None, expl_vlen=self.expl_vlen if self.expled else None, expl_lenindef=self.expl_lenindef, + ber_encoded=self.ber_encoded, + bered=self.bered, ) + for pp in self.pps_lenindef(decode_path): + yield pp class Enumerated(Integer): @@ -3154,7 +3360,10 @@ class Enumerated(Integer): if isinstance(value, self.__class__): value = value._value elif isinstance(value, integer_types): - if value not in list(self.specs.values()): + for _value in itervalues(self.specs): + if _value == value: + break + else: raise DecodeError( "unknown integer value: %s" % value, klass=self.__class__, @@ -3179,6 +3388,9 @@ class Enumerated(Integer): obj.offset = self.offset obj.llen = self.llen obj.vlen = self.vlen + obj.expl_lenindef = self.expl_lenindef + obj.lenindef = self.lenindef + obj.ber_encoded = self.ber_encoded return obj def __call__( @@ -3320,6 +3532,7 @@ class CommonString(OctetString): if self.ready: value = hexenc(bytes(self)) if no_unicode else self.__unicode__() yield _pp( + obj=self, asn1_type_name=self.asn1_type_name, obj_name=self.__class__.__name__, decode_path=decode_path, @@ -3337,7 +3550,11 @@ class CommonString(OctetString): expl_llen=self.expl_llen if self.expled else None, expl_vlen=self.expl_vlen if self.expled else None, expl_lenindef=self.expl_lenindef, + ber_encoded=self.ber_encoded, + bered=self.bered, ) + for pp in self.pps_lenindef(decode_path): + yield pp class UTF8String(CommonString): @@ -3347,29 +3564,57 @@ class UTF8String(CommonString): asn1_type_name = "UTF8String" -class NumericString(CommonString): +class AllowableCharsMixin(object): + @property + def allowable_chars(self): + if PY2: + return self._allowable_chars + return frozenset(six_unichr(c) for c in self._allowable_chars) + + +class NumericString(AllowableCharsMixin, CommonString): """Numeric string - Its value is properly sanitized: only ASCII digits can be stored. + Its value is properly sanitized: only ASCII digits with spaces can + be stored. + + >>> NumericString().allowable_chars + set(['3', '4', '7', '5', '1', '0', '8', '9', ' ', '6', '2']) """ __slots__ = () tag_default = tag_encode(18) encoding = "ascii" asn1_type_name = "NumericString" - allowable_chars = set(digits.encode("ascii")) + _allowable_chars = frozenset(digits.encode("ascii") + b" ") def _value_sanitize(self, value): value = super(NumericString, self)._value_sanitize(value) - if not set(value) <= self.allowable_chars: + if not frozenset(value) <= self._allowable_chars: raise DecodeError("non-numeric value") return value -class PrintableString(CommonString): +class PrintableString(AllowableCharsMixin, CommonString): + """Printable string + + Its value is properly sanitized: see X.680 41.4 table 10. + + >>> PrintableString().allowable_chars + >>> set([' ', "'", ..., 'z']) + """ __slots__ = () tag_default = tag_encode(19) encoding = "ascii" asn1_type_name = "PrintableString" + _allowable_chars = frozenset( + (ascii_letters + digits + " '()+,-./:=?").encode("ascii") + ) + + def _value_sanitize(self, value): + value = super(PrintableString, self)._value_sanitize(value) + if not frozenset(value) <= self._allowable_chars: + raise DecodeError("non-printable value") + return value class TeletexString(CommonString): @@ -3463,20 +3708,23 @@ class UTCTime(CommonString): self._value = default def _value_sanitize(self, value): - if isinstance(value, self.__class__): - return value._value - if isinstance(value, datetime): - return value.strftime(self.fmt).encode("ascii") if isinstance(value, binary_type): - value_decoded = value.decode("ascii") + try: + value_decoded = value.decode("ascii") + except (UnicodeEncodeError, UnicodeDecodeError) as err: + raise DecodeError("invalid UTCTime encoding") if len(value_decoded) == LEN_YYMMDDHHMMSSZ: try: datetime.strptime(value_decoded, self.fmt) - except ValueError: + except (TypeError, ValueError): raise DecodeError("invalid UTCTime format") return value else: raise DecodeError("invalid UTCTime length") + if isinstance(value, self.__class__): + return value._value + if isinstance(value, datetime): + return value.strftime(self.fmt).encode("ascii") raise InvalidValueType((self.__class__, datetime)) def __eq__(self, their): @@ -3517,6 +3765,7 @@ class UTCTime(CommonString): def pps(self, decode_path=()): yield _pp( + obj=self, asn1_type_name=self.asn1_type_name, obj_name=self.__class__.__name__, decode_path=decode_path, @@ -3534,7 +3783,11 @@ class UTCTime(CommonString): expl_llen=self.expl_llen if self.expled else None, expl_vlen=self.expl_vlen if self.expled else None, expl_lenindef=self.expl_lenindef, + ber_encoded=self.ber_encoded, + bered=self.bered, ) + for pp in self.pps_lenindef(decode_path): + yield pp class GeneralizedTime(UTCTime): @@ -3557,18 +3810,15 @@ class GeneralizedTime(UTCTime): fmt_ms = "%Y%m%d%H%M%S.%fZ" def _value_sanitize(self, value): - if isinstance(value, self.__class__): - return value._value - if isinstance(value, datetime): - return value.strftime( - self.fmt_ms if value.microsecond > 0 else self.fmt - ).encode("ascii") if isinstance(value, binary_type): - value_decoded = value.decode("ascii") + try: + value_decoded = value.decode("ascii") + except (UnicodeEncodeError, UnicodeDecodeError) as err: + raise DecodeError("invalid GeneralizedTime encoding") if len(value_decoded) == LEN_YYYYMMDDHHMMSSZ: try: datetime.strptime(value_decoded, self.fmt) - except ValueError: + except (TypeError, ValueError): raise DecodeError( "invalid GeneralizedTime (without ms) format", ) @@ -3576,7 +3826,7 @@ class GeneralizedTime(UTCTime): elif len(value_decoded) >= LEN_YYYYMMDDHHMMSSDMZ: try: datetime.strptime(value_decoded, self.fmt_ms) - except ValueError: + except (TypeError, ValueError): raise DecodeError( "invalid GeneralizedTime (with ms) format", ) @@ -3586,6 +3836,12 @@ class GeneralizedTime(UTCTime): "invalid GeneralizedTime length", klass=self.__class__, ) + if isinstance(value, self.__class__): + return value._value + if isinstance(value, datetime): + return value.strftime( + self.fmt_ms if value.microsecond > 0 else self.fmt + ).encode("ascii") raise InvalidValueType((self.__class__, datetime)) def todatetime(self): @@ -3711,8 +3967,6 @@ class Choice(Obj): self._value = default_obj.copy()._value def _value_sanitize(self, value): - if isinstance(value, self.__class__): - return value._value if isinstance(value, tuple) and len(value) == 2: choice, obj = value spec = self.specs.get(choice) @@ -3721,12 +3975,21 @@ class Choice(Obj): if not isinstance(obj, spec.__class__): raise InvalidValueType((spec,)) return (choice, spec(obj)) + if isinstance(value, self.__class__): + return value._value raise InvalidValueType((self.__class__, tuple)) @property def ready(self): return self._value is not None and self._value[1].ready + @property + def bered(self): + return self.expl_lenindef or ( + (self._value is not None) and + self._value[1].bered + ) + def copy(self): obj = self.__class__(schema=self.specs) obj._expl = self._expl @@ -3735,6 +3998,9 @@ class Choice(Obj): obj.offset = self.offset obj.llen = self.llen obj.vlen = self.vlen + obj.expl_lenindef = self.expl_lenindef + obj.lenindef = self.lenindef + obj.ber_encoded = self.ber_encoded value = self._value if value is not None: obj._value = (value[0], value[1].copy()) @@ -3806,7 +4072,7 @@ class Choice(Obj): return self._value[1].encode() def _decode(self, tlv, offset, decode_path, ctx, tag_only): - for choice, spec in self.specs.items(): + for choice, spec in iteritems(self.specs): sub_decode_path = decode_path + (choice,) try: spec.decode( @@ -3816,6 +4082,7 @@ class Choice(Obj): decode_path=sub_decode_path, ctx=ctx, tag_only=True, + _ctx_immutable=False, ) except TagMismatch: continue @@ -3826,7 +4093,7 @@ class Choice(Obj): decode_path=decode_path, offset=offset, ) - if tag_only: + if tag_only: # pragma: no cover return value, tail = spec.decode( tlv, @@ -3834,13 +4101,14 @@ class Choice(Obj): leavemm=True, decode_path=sub_decode_path, ctx=ctx, + _ctx_immutable=False, ) obj = self.__class__( schema=self.specs, expl=self._expl, default=self.default, optional=self.optional, - _decoded=(offset, 0, value.tlvlen), + _decoded=(offset, 0, value.fulllen), ) obj._value = (choice, value) return obj, tail @@ -3853,6 +4121,7 @@ class Choice(Obj): def pps(self, decode_path=()): yield _pp( + obj=self, asn1_type_name=self.asn1_type_name, obj_name=self.__class__.__name__, decode_path=decode_path, @@ -3866,9 +4135,12 @@ class Choice(Obj): llen=self.llen, vlen=self.vlen, expl_lenindef=self.expl_lenindef, + bered=self.bered, ) if self.ready: yield self.value.pps(decode_path=decode_path + (self.choice,)) + for pp in self.pps_lenindef(decode_path): + yield pp class PrimitiveTypes(Choice): @@ -3940,18 +4212,26 @@ class Any(Obj): self.defined = None def _value_sanitize(self, value): + if isinstance(value, binary_type): + return value if isinstance(value, self.__class__): return value._value if isinstance(value, Obj): return value.encode() - if isinstance(value, binary_type): - return value raise InvalidValueType((self.__class__, Obj, binary_type)) @property def ready(self): return self._value is not None + @property + def bered(self): + if self.expl_lenindef or self.lenindef: + return True + if self.defined is None: + return False + return self.defined[1].bered + def copy(self): obj = self.__class__() obj._value = self._value @@ -3961,6 +4241,9 @@ class Any(Obj): obj.offset = self.offset obj.llen = self.llen obj.vlen = self.vlen + obj.expl_lenindef = self.expl_lenindef + obj.lenindef = self.lenindef + obj.ber_encoded = self.ber_encoded return obj def __eq__(self, their): @@ -4024,6 +4307,7 @@ class Any(Obj): decode_path=decode_path + (str(chunk_i),), leavemm=True, ctx=ctx, + _ctx_immutable=False, ) vlen += chunk.tlvlen sub_offset += chunk.tlvlen @@ -4068,6 +4352,7 @@ class Any(Obj): def pps(self, decode_path=()): yield _pp( + obj=self, asn1_type_name=self.asn1_type_name, obj_name=self.__class__.__name__, decode_path=decode_path, @@ -4086,12 +4371,15 @@ class Any(Obj): expl_vlen=self.expl_vlen if self.expled else None, expl_lenindef=self.expl_lenindef, lenindef=self.lenindef, + bered=self.bered, ) defined_by, defined = self.defined or (None, None) if defined_by is not None: yield defined.pps( decode_path=decode_path + (DecodePathDefBy(defined_by),) ) + for pp in self.pps_lenindef(decode_path): + yield pp ######################################################################## @@ -4114,8 +4402,7 @@ def get_def_by_path(defines_by_path, sub_decode_path): def abs_decode_path(decode_path, rel_path): """Create an absolute decode path from current and relative ones - :param decode_path: current decode path, starting point. - Tuple of strings + :param decode_path: current decode path, starting point. Tuple of strings :param rel_path: relative path to ``decode_path``. Tuple of strings. If first tuple's element is "/", then treat it as an absolute path, ignoring ``decode_path`` as @@ -4195,7 +4482,7 @@ class Sequence(Obj): ("algorithm", ObjectIdentifier("1.2.3")), ("parameters", Any(Null())) )) - AlgorithmIdentifier SEQUENCE[OBJECT IDENTIFIER 1.2.3, ANY 0500 OPTIONAL] + AlgorithmIdentifier SEQUENCE[algorithm: OBJECT IDENTIFIER 1.2.3; parameters: ANY 0500 OPTIONAL] You can determine if value exists/set in the sequence and take its value: @@ -4216,18 +4503,14 @@ class Sequence(Obj): All defaulted values are always optional. - .. _strict_default_existence_ctx: - - .. warning:: + .. _allow_default_values_ctx: - When decoded DER contains defaulted value inside, then - technically this is not valid DER encoding. But we allow and pass - it **by default**. Of course reencoding of that kind of DER will - result in different binary representation (validly without - defaulted value inside). You can enable strict defaulted values - existence validation by setting ``"strict_default_existence": - True`` :ref:`context ` option -- decoding process will raise - an exception if defaulted value is met. + DER prohibits default value encoding and will raise an error if + default value is unexpectedly met during decode. + If :ref:`bered ` context option is set, then no error + will be raised, but ``bered`` attribute set. You can disable strict + defaulted values existence validation by setting + ``"allow_default_values": True`` :ref:`context ` option. Two sequences are equal if they have equal specification (schema), implicit/explicit tagging and the same values. @@ -4274,7 +4557,7 @@ class Sequence(Obj): @property def ready(self): - for name, spec in self.specs.items(): + for name, spec in iteritems(self.specs): value = self._value.get(name) if value is None: if spec.optional: @@ -4285,6 +4568,12 @@ class Sequence(Obj): return False return True + @property + def bered(self): + if self.expl_lenindef or self.lenindef or self.ber_encoded: + return True + return any(value.bered for value in itervalues(self._value)) + def copy(self): obj = self.__class__(schema=self.specs) obj.tag = self.tag @@ -4294,7 +4583,10 @@ class Sequence(Obj): obj.offset = self.offset obj.llen = self.llen obj.vlen = self.vlen - obj._value = {k: v.copy() for k, v in self._value.items()} + obj.expl_lenindef = self.expl_lenindef + obj.lenindef = self.lenindef + obj.ber_encoded = self.ber_encoded + obj._value = {k: v.copy() for k, v in iteritems(self._value)} return obj def __eq__(self, their): @@ -4355,7 +4647,7 @@ class Sequence(Obj): def _encoded_values(self): raws = [] - for name, spec in self.specs.items(): + for name, spec in iteritems(self.specs): value = self._value.get(name) if value is None: if spec.optional: @@ -4384,13 +4676,14 @@ class Sequence(Obj): decode_path=decode_path, offset=offset, ) - if tag_only: + if tag_only: # pragma: no cover return lenindef = False + ctx_bered = ctx.get("bered", False) try: l, llen, v = len_decode(lv) except LenIndefForm as err: - if not ctx.get("bered", False): + if not ctx_bered: raise err.__class__( msg=err.msg, klass=self.__class__, @@ -4418,7 +4711,9 @@ class Sequence(Obj): vlen = 0 sub_offset = offset + tlen + llen values = {} - for name, spec in self.specs.items(): + ber_encoded = False + ctx_allow_default_values = ctx.get("allow_default_values", False) + for name, spec in iteritems(self.specs): if spec.optional and ( (lenindef and v[:EOC_LEN].tobytes() == EOC) or len(v) == 0 @@ -4432,13 +4727,14 @@ class Sequence(Obj): leavemm=True, decode_path=sub_decode_path, ctx=ctx, + _ctx_immutable=False, ) except TagMismatch: if spec.optional: continue raise - defined = get_def_by_path(ctx.get("defines", ()), sub_decode_path) + defined = get_def_by_path(ctx.get("_defines", ()), sub_decode_path) if defined is not None: defined_by, defined_spec = defined if issubclass(value.__class__, SequenceOf): @@ -4456,6 +4752,7 @@ class Sequence(Obj): leavemm=True, decode_path=sub_sub_decode_path, ctx=ctx, + _ctx_immutable=False, ) if len(defined_tail) > 0: raise DecodeError( @@ -4475,6 +4772,7 @@ class Sequence(Obj): leavemm=True, decode_path=sub_decode_path + (DecodePathDefBy(defined_by),), ctx=ctx, + _ctx_immutable=False, ) if len(defined_tail) > 0: raise DecodeError( @@ -4485,20 +4783,20 @@ class Sequence(Obj): ) value.defined = (defined_by, defined_value) - value_len = value.expl_tlvlen if value.expled else value.tlvlen + value_len = value.fulllen vlen += value_len sub_offset += value_len v = v_tail if spec.default is not None and value == spec.default: - if ctx.get("strict_default_existence", False): + if ctx_bered or ctx_allow_default_values: + ber_encoded = True + else: raise DecodeError( "DEFAULT value met", klass=self.__class__, decode_path=sub_decode_path, offset=sub_offset, ) - else: - continue values[name] = value spec_defines = getattr(spec, "defines", ()) @@ -4510,7 +4808,7 @@ class Sequence(Obj): for rel_path, schema in spec_defines: defined = schema.get(value, None) if defined is not None: - ctx.setdefault("defines", []).append(( + ctx.setdefault("_defines", []).append(( abs_decode_path(sub_decode_path[:-1], rel_path), (value, defined), )) @@ -4541,6 +4839,7 @@ class Sequence(Obj): ) obj._value = values obj.lenindef = lenindef + obj.ber_encoded = ber_encoded return obj, tail def __repr__(self): @@ -4550,11 +4849,12 @@ class Sequence(Obj): _value = self._value.get(name) if _value is None: continue - cols.append(repr(_value)) - return "%s[%s]" % (value, ", ".join(cols)) + cols.append("%s: %s" % (name, repr(_value))) + return "%s[%s]" % (value, "; ".join(cols)) def pps(self, decode_path=()): yield _pp( + obj=self, asn1_type_name=self.asn1_type_name, obj_name=self.__class__.__name__, decode_path=decode_path, @@ -4572,18 +4872,30 @@ class Sequence(Obj): expl_vlen=self.expl_vlen if self.expled else None, expl_lenindef=self.expl_lenindef, lenindef=self.lenindef, + ber_encoded=self.ber_encoded, + bered=self.bered, ) for name in self.specs: value = self._value.get(name) if value is None: continue yield value.pps(decode_path=decode_path + (name,)) + for pp in self.pps_lenindef(decode_path): + yield pp class Set(Sequence): """``SET`` structure type Its usage is identical to :py:class:`pyderasn.Sequence`. + + .. _allow_unordered_set_ctx: + + DER prohibits unordered values encoding and will raise an error + during decode. If If :ref:`bered ` context option is set, + then no error will occure. Also you can disable strict values + ordering check by setting ``"allow_unordered_set": True`` + :ref:`context ` option. """ __slots__ = () tag_default = tag_encode(form=TagFormConstructed, num=17) @@ -4595,6 +4907,9 @@ class Set(Sequence): v = b"".join(raws) return b"".join((self.tag, len_encode(len(v)), v)) + def _specs_items(self): + return iteritems(self.specs) + def _decode(self, tlv, offset, decode_path, ctx, tag_only): try: t, tlen, lv = tag_strip(tlv) @@ -4614,10 +4929,11 @@ class Set(Sequence): if tag_only: return lenindef = False + ctx_bered = ctx.get("bered", False) try: l, llen, v = len_decode(lv) except LenIndefForm as err: - if not ctx.get("bered", False): + if not ctx_bered: raise err.__class__( msg=err.msg, klass=self.__class__, @@ -4644,11 +4960,15 @@ class Set(Sequence): vlen = 0 sub_offset = offset + tlen + llen values = {} - specs_items = self.specs.items + ber_encoded = False + ctx_allow_default_values = ctx.get("allow_default_values", False) + ctx_allow_unordered_set = ctx.get("allow_unordered_set", False) + value_prev = memoryview(v[:0]) + while len(v) > 0: if lenindef and v[:EOC_LEN].tobytes() == EOC: break - for name, spec in specs_items(): + for name, spec in self._specs_items(): sub_decode_path = decode_path + (name,) try: spec.decode( @@ -4658,6 +4978,7 @@ class Set(Sequence): decode_path=sub_decode_path, ctx=ctx, tag_only=True, + _ctx_immutable=False, ) except TagMismatch: continue @@ -4674,14 +4995,35 @@ class Set(Sequence): leavemm=True, decode_path=sub_decode_path, ctx=ctx, + _ctx_immutable=False, ) - value_len = value.expl_tlvlen if value.expled else value.tlvlen + value_len = value.fulllen + if value_prev.tobytes() > v[:value_len].tobytes(): + if ctx_bered or ctx_allow_unordered_set: + ber_encoded = True + else: + raise DecodeError( + "unordered " + self.asn1_type_name, + klass=self.__class__, + decode_path=sub_decode_path, + offset=sub_offset, + ) + if spec.default is None or value != spec.default: + pass + elif ctx_bered or ctx_allow_default_values: + ber_encoded = True + else: + raise DecodeError( + "DEFAULT value met", + klass=self.__class__, + decode_path=sub_decode_path, + offset=sub_offset, + ) + values[name] = value + value_prev = v[:value_len] sub_offset += value_len vlen += value_len v = v_tail - if spec.default is None or value != spec.default: # pragma: no cover - # SeqMixing.test_encoded_default_accepted covers that place - values[name] = value obj = self.__class__( schema=self.specs, impl=self.tag, @@ -4690,7 +5032,6 @@ class Set(Sequence): optional=self.optional, _decoded=(offset, llen, vlen + (EOC_LEN if lenindef else 0)), ) - obj._value = values if lenindef: if v[:EOC_LEN].tobytes() != EOC: raise DecodeError( @@ -4701,6 +5042,7 @@ class Set(Sequence): ) tail = v[EOC_LEN:] obj.lenindef = True + obj._value = values if not obj.ready: raise DecodeError( "not all values are ready", @@ -4708,6 +5050,7 @@ class Set(Sequence): decode_path=decode_path, offset=offset, ) + obj.ber_encoded = ber_encoded return obj, tail @@ -4806,6 +5149,12 @@ class SequenceOf(Obj): def ready(self): return all(v.ready for v in self._value) + @property + def bered(self): + if self.expl_lenindef or self.lenindef or self.ber_encoded: + return True + return any(v.bered for v in self._value) + def copy(self): obj = self.__class__(schema=self.spec) obj._bound_min = self._bound_min @@ -4817,6 +5166,9 @@ class SequenceOf(Obj): obj.offset = self.offset obj.llen = self.llen obj.vlen = self.vlen + obj.expl_lenindef = self.expl_lenindef + obj.lenindef = self.lenindef + obj.ber_encoded = self.ber_encoded obj._value = [v.copy() for v in self._value] return obj @@ -4891,7 +5243,7 @@ class SequenceOf(Obj): v = b"".join(self._encoded_values()) return b"".join((self.tag, len_encode(len(v)), v)) - def _decode(self, tlv, offset, decode_path, ctx, tag_only): + def _decode(self, tlv, offset, decode_path, ctx, tag_only, ordering_check=False): try: t, tlen, lv = tag_strip(tlv) except DecodeError as err: @@ -4910,10 +5262,11 @@ class SequenceOf(Obj): if tag_only: return lenindef = False + ctx_bered = ctx.get("bered", False) try: l, llen, v = len_decode(lv) except LenIndefForm as err: - if not ctx.get("bered", False): + if not ctx_bered: raise err.__class__( msg=err.msg, klass=self.__class__, @@ -4941,32 +5294,57 @@ class SequenceOf(Obj): vlen = 0 sub_offset = offset + tlen + llen _value = [] + ctx_allow_unordered_set = ctx.get("allow_unordered_set", False) + value_prev = memoryview(v[:0]) + ber_encoded = False spec = self.spec while len(v) > 0: if lenindef and v[:EOC_LEN].tobytes() == EOC: break + sub_decode_path = decode_path + (str(len(_value)),) value, v_tail = spec.decode( v, sub_offset, leavemm=True, - decode_path=decode_path + (str(len(_value)),), + decode_path=sub_decode_path, ctx=ctx, + _ctx_immutable=False, ) - value_len = value.expl_tlvlen if value.expled else value.tlvlen + value_len = value.fulllen + if ordering_check: + if value_prev.tobytes() > v[:value_len].tobytes(): + if ctx_bered or ctx_allow_unordered_set: + ber_encoded = True + else: + raise DecodeError( + "unordered " + self.asn1_type_name, + klass=self.__class__, + decode_path=sub_decode_path, + offset=sub_offset, + ) + value_prev = v[:value_len] + _value.append(value) sub_offset += value_len vlen += value_len v = v_tail - _value.append(value) - obj = self.__class__( - value=_value, - schema=spec, - bounds=(self._bound_min, self._bound_max), - impl=self.tag, - expl=self._expl, - default=self.default, - optional=self.optional, - _decoded=(offset, llen, vlen + (EOC_LEN if lenindef else 0)), - ) + try: + obj = self.__class__( + value=_value, + schema=spec, + bounds=(self._bound_min, self._bound_max), + impl=self.tag, + expl=self._expl, + default=self.default, + optional=self.optional, + _decoded=(offset, llen, vlen + (EOC_LEN if lenindef else 0)), + ) + except BoundsError as err: + raise DecodeError( + msg=str(err), + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) if lenindef: if v[:EOC_LEN].tobytes() != EOC: raise DecodeError( @@ -4977,6 +5355,7 @@ class SequenceOf(Obj): ) obj.lenindef = True tail = v[EOC_LEN:] + obj.ber_encoded = ber_encoded return obj, tail def __repr__(self): @@ -4987,6 +5366,7 @@ class SequenceOf(Obj): def pps(self, decode_path=()): yield _pp( + obj=self, asn1_type_name=self.asn1_type_name, obj_name=self.__class__.__name__, decode_path=decode_path, @@ -5004,9 +5384,13 @@ class SequenceOf(Obj): expl_vlen=self.expl_vlen if self.expled else None, expl_lenindef=self.expl_lenindef, lenindef=self.lenindef, + ber_encoded=self.ber_encoded, + bered=self.bered, ) for i, value in enumerate(self._value): yield value.pps(decode_path=decode_path + (str(i),)) + for pp in self.pps_lenindef(decode_path): + yield pp class SetOf(SequenceOf): @@ -5024,6 +5408,16 @@ class SetOf(SequenceOf): v = b"".join(raws) return b"".join((self.tag, len_encode(len(v)), v)) + def _decode(self, tlv, offset, decode_path, ctx, tag_only): + return super(SetOf, self)._decode( + tlv, + offset, + decode_path, + ctx, + tag_only, + ordering_check=True, + ) + def obj_by_path(pypath): # pragma: no cover """Import object specified as string Python path @@ -5048,7 +5442,7 @@ def generic_decoder(): # pragma: no cover choice = PrimitiveTypes() choice.specs["SequenceOf"] = SequenceOf(schema=choice) choice.specs["SetOf"] = SetOf(schema=choice) - for i in range(31): + for i in six_xrange(31): choice.specs["SequenceOf%d" % i] = SequenceOf( schema=choice, expl=tag_ctxc(i), @@ -5060,10 +5454,21 @@ def generic_decoder(): # pragma: no cover __slots__ = () schema = choice - def pprint_any(obj, oids=None, with_colours=False): + def pprint_any( + obj, + oids=None, + with_colours=False, + with_decode_path=False, + decode_path_only=(), + ): def _pprint_pps(pps): for pp in pps: if hasattr(pp, "_fields"): + if ( + decode_path_only != () and + pp.decode_path[:len(decode_path_only)] != decode_path_only + ): + continue if pp.asn1_type_name == Choice.asn1_type_name: continue pp_kwargs = pp._asdict() @@ -5075,8 +5480,13 @@ def generic_decoder(): # pragma: no cover with_offsets=True, with_blob=False, with_colours=with_colours, + with_decode_path=with_decode_path, + decode_path_len_decrease=len(decode_path_only), ) - for row in pp_console_blob(pp): + for row in pp_console_blob( + pp, + decode_path_len_decrease=len(decode_path_only), + ): yield row else: for row in _pprint_pps(pp): @@ -5108,9 +5518,23 @@ def main(): # pragma: no cover ) parser.add_argument( "--nobered", - action='store_true', + action="store_true", help="Disallow BER encoding", ) + parser.add_argument( + "--print-decode-path", + action="store_true", + help="Print decode paths", + ) + parser.add_argument( + "--decode-path-only", + help="Print only specified decode path", + ) + parser.add_argument( + "--allow-expl-oob", + action="store_true", + help="Allow explicit tag out-of-bound", + ) parser.add_argument( "DERFile", type=argparse.FileType("rb"), @@ -5127,7 +5551,10 @@ def main(): # pragma: no cover pprinter = partial(pprint, big_blobs=True) else: schema, pprinter = generic_decoder() - ctx = {"bered": not args.nobered} + ctx = { + "bered": not args.nobered, + "allow_expl_oob": args.allow_expl_oob, + } if args.defines_by_path is not None: ctx["defines_by_path"] = obj_by_path(args.defines_by_path) obj, tail = schema().decode(der, ctx=ctx) @@ -5135,6 +5562,11 @@ def main(): # pragma: no cover obj, oids=oids, with_colours=True if environ.get("NO_COLOR") is None else False, + with_decode_path=args.print_decode_path, + decode_path_only=( + () if args.decode_path_only is None else + tuple(args.decode_path_only.split(":")) + ), )) if tail != b"": print("\nTrailing data: %s" % hexenc(tail))