]> Cypherpunks.ru repositories - pyderasn.git/blobdiff - pyderasn.py
Fix DecodeError bug with DecodePathDefBy
[pyderasn.git] / pyderasn.py
index 73eec5ead5d3fbd1558d45e2fa4e5d2ad17ec9ee..3ed23da12c2fb486359224fc1275413b9da44980 100755 (executable)
@@ -195,6 +195,20 @@ lesser than ``offset``), ``expl_tlen``, ``expl_llen``, ``expl_vlen``
 
 When error occurs, then :py:exc:`pyderasn.DecodeError` is raised.
 
+.. _ctx:
+
+Context
+_______
+
+You can specify so called context keyword argument during ``decode()``
+invocation. It is dictionary containing various options governing
+decoding process.
+
+Currently available context options:
+
+* :ref:`defines_by_path <defines_by_path_ctx>`
+* :ref:`strict_default_existence <strict_default_existence_ctx>`
+
 .. _pprinting:
 
 Pretty printing
@@ -230,15 +244,15 @@ _____________
 
 :py:class:`pyderasn.ObjectIdentifier` field inside
 :py:class:`pyderasn.Sequence` can hold mapping between OIDs and
-necessary for decoding structrures. For example, CMS (:rfc:`5652`)
+necessary for decoding structures. For example, CMS (:rfc:`5652`)
 container::
 
     class ContentInfo(Sequence):
         schema = (
-            ("contentType", ContentType(defines=("content", {
+            ("contentType", ContentType(defines=((("content",), {
                 id_digestedData: DigestedData(),
                 id_signedData: SignedData(),
-            }))),
+            }),))),
             ("content", Any(expl=tag_ctxc(0))),
         )
 
@@ -248,6 +262,27 @@ decoded with ``SignedData`` specification, if ``contentType`` equals to
 ``contentType`` contains unknown OID, then no automatic decoding is
 done.
 
+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``::
+
+        (
+            (('parameters',), {
+                id_ecPublicKey: ECParameters(),
+                id_GostR3410_2001: GostR34102001PublicKeyParameters(),
+            }),
+            (('..', 'subjectPublicKey'), {
+                id_rsaEncryption: RSAPublicKey(),
+                id_GostR3410_2001: OctetString(),
+            }),
+        ),
+
+tells that if certificate's SPKI algorithm is GOST R 34.10-2001, then
+autodecode its parameters inside SPKI's algorithm and its public key
+itself.
+
 Following types can be automatically decoded (DEFINED BY):
 
 * :py:class:`pyderasn.Any`
@@ -262,17 +297,17 @@ was defined, ``value`` contains corresponding decoded value. For example
 above, ``content_info["content"].defined == (id_signedData,
 signed_data)``.
 
-.. _defines_by_path_kwarg:
+.. _defines_by_path_ctx:
 
-defines_by_path kwarg
-_____________________
+defines_by_path context option
+______________________________
 
 Sometimes you either can not or do not want to explicitly set *defines*
 in the scheme. You can dynamically apply those definitions when calling
 ``.decode()`` method.
 
-Decode method takes optional ``defines_by_path`` keyword argument that
-must be sequence of following tuples::
+Specify ``defines_by_path`` key in the :ref:`decode context <ctx>`. Its
+value must be sequence of following tuples::
 
     (decode_path, defines)
 
@@ -289,41 +324,41 @@ of ``PKIResponse``::
     content_info, tail = ContentInfo().decode(data, defines_by_path=(
         (
             ("contentType",),
-            ("content", {id_signedData: SignedData()}),
+            ((("content",), {id_signedData: SignedData()}),),
         ),
         (
             (
                 "content",
-                decode_path_defby(id_signedData),
+                DecodePathDefBy(id_signedData),
                 "encapContentInfo",
                 "eContentType",
             ),
-            ("eContent", {
+            ((("eContent",), {
                 id_cct_PKIData: PKIData(),
                 id_cct_PKIResponse: PKIResponse(),
-            }),
+            })),
         ),
         (
             (
                 "content",
-                decode_path_defby(id_signedData),
+                DecodePathDefBy(id_signedData),
                 "encapContentInfo",
                 "eContent",
-                decode_path_defby(id_cct_PKIResponse),
+                DecodePathDefBy(id_cct_PKIResponse),
                 "controlSequence",
                 any,
                 "attrType",
             ),
-            ("attrValues", {
+            ((("attrValues",), {
                 id_cmc_recipientNonce: RecipientNonce(),
                 id_cmc_senderNonce: SenderNonce(),
                 id_cmc_statusInfoV2: CMCStatusInfoV2(),
                 id_cmc_transactionId: TransactionId(),
-            }),
+            })),
         ),
     ))
 
-Pay attention for :py:func:`pyderasn.decode_path_defby` and ``any``.
+Pay attention for :py:class:`pyderasn.DecodePathDefBy` and ``any``.
 First function is useful for path construction when some automatic
 decoding is already done. ``any`` means literally any value it meet --
 useful for SEQUENCE/SET OF-s.
@@ -421,6 +456,7 @@ _____
 Various
 -------
 
+.. autofunction:: pyderasn.abs_decode_path
 .. autofunction:: pyderasn.hexenc
 .. autofunction:: pyderasn.hexdec
 .. autofunction:: pyderasn.tag_encode
@@ -450,6 +486,13 @@ from six import text_type
 from six.moves import xrange as six_xrange
 
 
+try:
+    from termcolor import colored
+except ImportError:
+    def colored(what, *args):
+        return what
+
+
 __all__ = (
     "Any",
     "BitString",
@@ -457,8 +500,8 @@ __all__ = (
     "Boolean",
     "BoundsError",
     "Choice",
-    "decode_path_defby",
     "DecodeError",
+    "DecodePathDefBy",
     "Enumerated",
     "GeneralizedTime",
     "GeneralString",
@@ -545,7 +588,7 @@ class DecodeError(Exception):
             c for c in (
                 "" if self.klass is None else self.klass.__name__,
                 (
-                    ("(%s)" % ".".join(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 "",
@@ -867,7 +910,7 @@ class Obj(object):
     def _encode(self):  # pragma: no cover
         raise NotImplementedError()
 
-    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):  # pragma: no cover
+    def _decode(self, tlv, offset, decode_path, ctx):  # pragma: no cover
         raise NotImplementedError()
 
     def encode(self):
@@ -876,23 +919,25 @@ class Obj(object):
             return raw
         return b"".join((self._expl, len_encode(len(raw)), raw))
 
-    def decode(self, data, offset=0, leavemm=False, decode_path=(), defines_by_path=None):
+    def decode(self, data, offset=0, leavemm=False, decode_path=(), ctx=None):
         """Decode the data
 
         :param data: either binary or memoryview
         :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 defines_by_path: :ref:`Read about DEFINED BY <definedby>`
+        :param ctx: optional :ref:`context <ctx>` governing decoding process.
         :returns: (Obj, remaining data)
         """
+        if ctx is None:
+            ctx = {}
         tlv = memoryview(data)
         if self._expl is None:
             obj, tail = self._decode(
                 tlv,
                 offset,
                 decode_path=decode_path,
-                defines_by_path=defines_by_path,
+                ctx=ctx,
             )
         else:
             try:
@@ -930,7 +975,7 @@ class Obj(object):
                 v,
                 offset=offset + tlen + llen,
                 decode_path=decode_path,
-                defines_by_path=defines_by_path,
+                ctx=ctx,
             )
         return obj, (tail if leavemm else tail.tobytes())
 
@@ -963,10 +1008,24 @@ class Obj(object):
         return self.expl_tlen + self.expl_llen + self.expl_vlen
 
 
-def decode_path_defby(defined_by):
+class DecodePathDefBy(object):
     """DEFINED BY representation inside decode path
     """
-    return "DEFINED BY (%s)" % defined_by
+    __slots__ = ('defined_by',)
+
+    def __init__(self, defined_by):
+        self.defined_by = defined_by
+
+    def __eq__(self, their):
+        if not isinstance(their, self.__class__):
+            return False
+        return self.defined_by == their.defined_by
+
+    def __str__(self):
+        return "DEFINED BY " + str(self.defined_by)
+
+    def __repr__(self):
+        return "<%s: %s>" % (self.__class__.__name__, self.defined_by)
 
 
 ########################################################################
@@ -1034,49 +1093,75 @@ def _pp(
     )
 
 
-def pp_console_row(pp, oids=None, with_offsets=False, with_blob=True):
+def _colorize(what, colour, with_colours, attrs=("bold",)):
+    return colored(what, colour, attrs=attrs) if with_colours else what
+
+
+def pp_console_row(
+        pp,
+        oids=None,
+        with_offsets=False,
+        with_blob=True,
+        with_colours=False,
+):
     cols = []
     if with_offsets:
-        cols.append("%5d%s [%d,%d,%4d]" % (
+        col = "%5d%s" % (
             pp.offset,
             (
                 "  " if pp.expl_offset is None else
                 ("-%d" % (pp.offset - pp.expl_offset))
             ),
-            pp.tlen,
-            pp.llen,
-            pp.vlen,
-        ))
+        )
+        cols.append(_colorize(col, "red", with_colours, ()))
+        col = "[%d,%d,%4d]" % (pp.tlen, pp.llen, pp.vlen)
+        cols.append(_colorize(col, "green", with_colours, ()))
     if len(pp.decode_path) > 0:
         cols.append(" ." * (len(pp.decode_path)))
-        cols.append("%s:" % pp.decode_path[-1])
+        ent = pp.decode_path[-1]
+        if isinstance(ent, DecodePathDefBy):
+            cols.append(_colorize("DEFINED BY", "red", with_colours, ("reverse",)))
+            value = str(ent.defined_by)
+            if (
+                    oids is not None and
+                    ent.defined_by.asn1_type_name ==
+                    ObjectIdentifier.asn1_type_name and
+                    value in oids
+            ):
+                cols.append(_colorize("%s:" % oids[value], "green", with_colours))
+            else:
+                cols.append(_colorize("%s:" % value, "white", with_colours))
+        else:
+            cols.append(_colorize("%s:" % ent, "yellow", with_colours))
     if pp.expl is not None:
         klass, _, num = pp.expl
-        cols.append("[%s%d] EXPLICIT" % (TagClassReprs[klass], num))
+        col = "[%s%d] EXPLICIT" % (TagClassReprs[klass], num)
+        cols.append(_colorize(col, "blue", with_colours))
     if pp.impl is not None:
         klass, _, num = pp.impl
-        cols.append("[%s%d]" % (TagClassReprs[klass], num))
+        col = "[%s%d]" % (TagClassReprs[klass], num)
+        cols.append(_colorize(col, "blue", with_colours))
     if pp.asn1_type_name.replace(" ", "") != pp.obj_name.upper():
-        cols.append(pp.obj_name)
-    cols.append(pp.asn1_type_name)
+        cols.append(_colorize(pp.obj_name, "magenta", with_colours))
+    cols.append(_colorize(pp.asn1_type_name, "cyan", with_colours))
     if pp.value is not None:
         value = pp.value
+        cols.append(_colorize(value, "white", with_colours))
         if (
                 oids is not None and
                 pp.asn1_type_name == ObjectIdentifier.asn1_type_name and
                 value in oids
         ):
-            value = "%s (%s)" % (oids[value], pp.value)
-        cols.append(value)
+            cols.append(_colorize("(%s)" % oids[value], "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("OPTIONAL")
+        cols.append(_colorize("OPTIONAL", "red", with_colours))
     if pp.default:
-        cols.append("DEFAULT")
+        cols.append(_colorize("DEFAULT", "red", with_colours))
     return " ".join(cols)
 
 
@@ -1095,7 +1180,7 @@ def pp_console_blob(pp):
         yield " ".join(cols + [", ".join(pp.blob)])
 
 
-def pprint(obj, oids=None, big_blobs=False):
+def pprint(obj, oids=None, big_blobs=False, with_colours=False):
     """Pretty print object
 
     :param Obj obj: object you want to pretty print
@@ -1104,6 +1189,8 @@ def pprint(obj, oids=None, big_blobs=False):
     :param big_blobs: if large binary objects are met (like OctetString
                       values), do we need to print them too, on separate
                       lines
+    :param with_colours: colourize output, if ``termcolor`` library
+                         is available
     """
     def _pprint_pps(pps):
         for pp in pps:
@@ -1114,11 +1201,18 @@ def pprint(obj, oids=None, big_blobs=False):
                         oids=oids,
                         with_offsets=True,
                         with_blob=False,
+                        with_colours=with_colours,
                     )
                     for row in pp_console_blob(pp):
                         yield row
                 else:
-                    yield pp_console_row(pp, oids=oids, with_offsets=True)
+                    yield pp_console_row(
+                        pp,
+                        oids=oids,
+                        with_offsets=True,
+                        with_blob=True,
+                        with_colours=with_colours,
+                    )
             else:
                 for row in _pprint_pps(pp):
                     yield row
@@ -1238,7 +1332,7 @@ class Boolean(Obj):
             (b"\xFF" if self._value else b"\x00"),
         ))
 
-    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
+    def _decode(self, tlv, offset, decode_path, ctx):
         try:
             t, _, lv = tag_strip(tlv)
         except DecodeError as err:
@@ -1531,7 +1625,7 @@ class Integer(Obj):
                     break
         return b"".join((self.tag, len_encode(len(octets)), octets))
 
-    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
+    def _decode(self, tlv, offset, decode_path, ctx):
         try:
             t, _, lv = tag_strip(tlv)
         except DecodeError as err:
@@ -1865,7 +1959,7 @@ class BitString(Obj):
             octets,
         ))
 
-    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
+    def _decode(self, tlv, offset, decode_path, ctx):
         try:
             t, _, lv = tag_strip(tlv)
         except DecodeError as err:
@@ -1971,7 +2065,7 @@ class BitString(Obj):
         defined_by, defined = self.defined or (None, None)
         if defined_by is not None:
             yield defined.pps(
-                decode_path=decode_path + (decode_path_defby(defined_by),)
+                decode_path=decode_path + (DecodePathDefBy(defined_by),)
             )
 
 
@@ -2117,7 +2211,7 @@ class OctetString(Obj):
             self._value,
         ))
 
-    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
+    def _decode(self, tlv, offset, decode_path, ctx):
         try:
             t, _, lv = tag_strip(tlv)
         except DecodeError as err:
@@ -2195,7 +2289,7 @@ class OctetString(Obj):
         defined_by, defined = self.defined or (None, None)
         if defined_by is not None:
             yield defined.pps(
-                decode_path=decode_path + (decode_path_defby(defined_by),)
+                decode_path=decode_path + (DecodePathDefBy(defined_by),)
             )
 
 
@@ -2266,7 +2360,7 @@ class Null(Obj):
     def _encode(self):
         return self.tag + len_encode(0)
 
-    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
+    def _decode(self, tlv, offset, decode_path, ctx):
         try:
             t, _, lv = tag_strip(tlv)
         except DecodeError as err:
@@ -2353,7 +2447,7 @@ class ObjectIdentifier(Obj):
     def __init__(
             self,
             value=None,
-            defines=None,
+            defines=(),
             impl=None,
             expl=None,
             default=None,
@@ -2364,12 +2458,14 @@ class ObjectIdentifier(Obj):
         :param value: set the value. Either tuples of integers,
                       string of "."-concatenated integers, or
                       :py:class:`pyderasn.ObjectIdentifier` object
-        :param defines: tuple of two elements. First one is a name of
-                        field inside :py:class:`pyderasn.Sequence`,
-                        defining with that OID. Second element is a
-                        ``{OID: pyderasn.Obj()}`` dictionary, mapping
-                        between current OID value and structure applied
-                        to defined field.
+        :param defines: sequence of tuples. Each tuple has two elements.
+                        First one is relative to current one decode
+                        path, aiming to the field defined by that OID.
+                        Read about relative path in
+                        :py:func:`pyderasn.abs_decode_path`. Second
+                        tuple element is ``{OID: pyderasn.Obj()}``
+                        dictionary, mapping between current OID value
+                        and structure applied to defined field.
                         :ref:`Read about DEFINED BY <definedby>`
         :param bytes impl: override default tag with ``IMPLICIT`` one
         :param bytes expl: override default tag with ``EXPLICIT`` one
@@ -2509,7 +2605,7 @@ class ObjectIdentifier(Obj):
         v = b"".join(octets)
         return b"".join((self.tag, len_encode(len(v)), v))
 
-    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
+    def _decode(self, tlv, offset, decode_path, ctx):
         try:
             t, _, lv = tag_strip(tlv)
         except DecodeError as err:
@@ -3276,7 +3372,7 @@ class Choice(Obj):
         self._assert_ready()
         return self._value[1].encode()
 
-    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
+    def _decode(self, tlv, offset, decode_path, ctx):
         for choice, spec in self.specs.items():
             try:
                 value, tail = spec.decode(
@@ -3284,7 +3380,7 @@ class Choice(Obj):
                     offset=offset,
                     leavemm=True,
                     decode_path=decode_path + (choice,),
-                    defines_by_path=defines_by_path,
+                    ctx=ctx,
                 )
             except TagMismatch:
                 continue
@@ -3451,7 +3547,7 @@ class Any(Obj):
         self._assert_ready()
         return self._value
 
-    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
+    def _decode(self, tlv, offset, decode_path, ctx):
         try:
             t, tlen, lv = tag_strip(tlv)
             l, llen, v = len_decode(lv)
@@ -3505,7 +3601,7 @@ class Any(Obj):
         defined_by, defined = self.defined or (None, None)
         if defined_by is not None:
             yield defined.pps(
-                decode_path=decode_path + (decode_path_defby(defined_by),)
+                decode_path=decode_path + (DecodePathDefBy(defined_by),)
             )
 
 
@@ -3526,6 +3622,32 @@ def get_def_by_path(defines_by_path, sub_decode_path):
             return define
 
 
+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 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
+                     starting point. Also this tuple can contain ".."
+                     elements, stripping the leading element from
+                     ``decode_path``
+
+    >>> abs_decode_path(("foo", "bar"), ("baz", "whatever"))
+    ("foo", "bar", "baz", "whatever")
+    >>> abs_decode_path(("foo", "bar", "baz"), ("..", "..", "whatever"))
+    ("foo", "whatever")
+    >>> abs_decode_path(("foo", "bar"), ("/", "baz", "whatever"))
+    ("baz", "whatever")
+    """
+    if rel_path[0] == "/":
+        return rel_path[1:]
+    if rel_path[0] == "..":
+        return abs_decode_path(decode_path[:-1], rel_path[1:])
+    return decode_path + rel_path
+
+
 class Sequence(Obj):
     """``SEQUENCE`` structure type
 
@@ -3576,6 +3698,8 @@ class Sequence(Obj):
     >>> tbs = TBSCertificate()
     >>> tbs["version"] = Version("v2") # no need to explicitly add ``expl``
 
+    Assign ``None`` to remove value from sequence.
+
     You can know if value exists/set in the sequence and take its value:
 
     >>> "extnID" in ext, "extnValue" in ext, "critical" in ext
@@ -3595,13 +3719,18 @@ class Sequence(Obj):
 
     All defaulted values are always optional.
 
+    .. _strict_default_existence_ctx:
+
     .. warning::
 
        When decoded DER contains defaulted value inside, then
-       technically this is not valid DER encoding. But we allow
-       and pass it. Of course reencoding of that kind of DER will
+       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).
+       defaulted value inside). You can enable strict defaulted values
+       existence validation by setting ``"strict_default_existence":
+       True`` :ref:`context <ctx>` option -- decoding process will raise
+       an exception if defaulted value is met.
 
     Two sequences are equal if they have equal specification (schema),
     implicit/explicit tagging and the same values.
@@ -3739,7 +3868,7 @@ class Sequence(Obj):
         v = b"".join(self._encoded_values())
         return b"".join((self.tag, len_encode(len(v)), v))
 
-    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
+    def _decode(self, tlv, offset, decode_path, ctx):
         try:
             t, tlen, lv = tag_strip(tlv)
         except DecodeError as err:
@@ -3774,7 +3903,6 @@ class Sequence(Obj):
         v, tail = v[:l], v[l:]
         sub_offset = offset + tlen + llen
         values = {}
-        defines = {}
         for name, spec in self.specs.items():
             if len(v) == 0 and spec.optional:
                 continue
@@ -3785,28 +3913,31 @@ class Sequence(Obj):
                     sub_offset,
                     leavemm=True,
                     decode_path=sub_decode_path,
-                    defines_by_path=defines_by_path,
+                    ctx=ctx,
                 )
             except TagMismatch:
                 if spec.optional:
                     continue
                 raise
 
-            defined = defines.pop(name, None)
+            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):
                     for i, _value in enumerate(value):
                         sub_sub_decode_path = sub_decode_path + (
                             str(i),
-                            decode_path_defby(defined_by),
+                            DecodePathDefBy(defined_by),
                         )
                         defined_value, defined_tail = defined_spec.decode(
                             memoryview(bytes(_value)),
-                            sub_offset + value.tlen + value.llen,
+                            sub_offset + (
+                                (value.tlen + value.llen + value.expl_tlen + value.expl_llen)
+                                if value.expled else (value.tlen + value.llen)
+                            ),
                             leavemm=True,
                             decode_path=sub_sub_decode_path,
-                            defines_by_path=defines_by_path,
+                            ctx=ctx,
                         )
                         if len(defined_tail) > 0:
                             raise DecodeError(
@@ -3819,16 +3950,19 @@ class Sequence(Obj):
                 else:
                     defined_value, defined_tail = defined_spec.decode(
                         memoryview(bytes(value)),
-                        sub_offset + value.tlen + value.llen,
+                        sub_offset + (
+                            (value.tlen + value.llen + value.expl_tlen + value.expl_llen)
+                            if value.expled else (value.tlen + value.llen)
+                        ),
                         leavemm=True,
-                        decode_path=sub_decode_path + (decode_path_defby(defined_by),),
-                        defines_by_path=defines_by_path,
+                        decode_path=sub_decode_path + (DecodePathDefBy(defined_by),),
+                        ctx=ctx,
                     )
                     if len(defined_tail) > 0:
                         raise DecodeError(
                             "remaining data",
                             klass=self.__class__,
-                            decode_path=sub_decode_path + (decode_path_defby(defined_by),),
+                            decode_path=sub_decode_path + (DecodePathDefBy(defined_by),),
                             offset=offset,
                         )
                     value.defined = (defined_by, defined_value)
@@ -3836,19 +3970,30 @@ class Sequence(Obj):
             sub_offset += (value.expl_tlvlen if value.expled else value.tlvlen)
             v = v_tail
             if spec.default is not None and value == spec.default:
-                # Encoded default values are not valid in DER,
-                # but we allow that anyway
-                continue
+                if ctx.get("strict_default_existence", False):
+                    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", None)
-            if defines_by_path is not None and spec_defines is None:
-                spec_defines = get_def_by_path(defines_by_path, sub_decode_path)
-            if spec_defines is not None:
-                what, schema = spec_defines
-                defined = schema.get(value, None)
-                if defined is not None:
-                    defines[what] = (value, defined)
+            spec_defines = getattr(spec, "defines", ())
+            if len(spec_defines) == 0:
+                defines_by_path = ctx.get("defines_by_path", ())
+                if len(defines_by_path) > 0:
+                    spec_defines = get_def_by_path(defines_by_path, sub_decode_path)
+            if spec_defines is not None and len(spec_defines) > 0:
+                for rel_path, schema in spec_defines:
+                    defined = schema.get(value, None)
+                    if defined is not None:
+                        ctx.setdefault("defines", []).append((
+                            abs_decode_path(sub_decode_path[:-1], rel_path),
+                            (value, defined),
+                        ))
         if len(v) > 0:
             raise DecodeError(
                 "remaining data",
@@ -3917,7 +4062,7 @@ class Set(Sequence):
         v = b"".join(raws)
         return b"".join((self.tag, len_encode(len(v)), v))
 
-    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
+    def _decode(self, tlv, offset, decode_path, ctx):
         try:
             t, tlen, lv = tag_strip(tlv)
         except DecodeError as err:
@@ -3960,7 +4105,7 @@ class Set(Sequence):
                         sub_offset,
                         leavemm=True,
                         decode_path=decode_path + (name,),
-                        defines_by_path=defines_by_path,
+                        ctx=ctx,
                     )
                 except TagMismatch:
                     continue
@@ -4170,7 +4315,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=0, decode_path=(), defines_by_path=None):
+    def _decode(self, tlv, offset, decode_path, ctx):
         try:
             t, tlen, lv = tag_strip(tlv)
         except DecodeError as err:
@@ -4212,7 +4357,7 @@ class SequenceOf(Obj):
                 sub_offset,
                 leavemm=True,
                 decode_path=decode_path + (str(len(_value)),),
-                defines_by_path=defines_by_path,
+                ctx=ctx,
             )
             sub_offset += (value.expl_tlvlen if value.expled else value.tlvlen)
             v = v_tail
@@ -4308,7 +4453,7 @@ def generic_decoder():  # pragma: no cover
         __slots__ = ()
         schema = choice
 
-    def pprint_any(obj, oids=None):
+    def pprint_any(obj, oids=None, with_colours=False):
         def _pprint_pps(pps):
             for pp in pps:
                 if hasattr(pp, "_fields"):
@@ -4322,6 +4467,7 @@ def generic_decoder():  # pragma: no cover
                         oids=oids,
                         with_offsets=True,
                         with_blob=False,
+                        with_colours=with_colours,
                     )
                     for row in pp_console_blob(pp):
                         yield row
@@ -4353,6 +4499,11 @@ def main():  # pragma: no cover
         "--defines-by-path",
         help="Python path to decoder's defines_by_path",
     )
+    parser.add_argument(
+        "--with-colours",
+        action='store_true',
+        help="Enable coloured output",
+    )
     parser.add_argument(
         "DERFile",
         type=argparse.FileType("rb"),
@@ -4371,12 +4522,16 @@ def main():  # pragma: no cover
         schema, pprinter = generic_decoder()
     obj, tail = schema().decode(
         der,
-        defines_by_path=(
-            None if args.defines_by_path is None
-            else obj_by_path(args.defines_by_path)
+        ctx=(
+            None if args.defines_by_path is None else
+            {"defines_by_path": obj_by_path(args.defines_by_path)}
         ),
     )
-    print(pprinter(obj, oids=oids))
+    print(pprinter(
+        obj,
+        oids=oids,
+        with_colours=True if args.with_colours else False,
+    ))
     if tail != b"":
         print("\nTrailing data: %s" % hexenc(tail))