]> Cypherpunks.ru repositories - pyderasn.git/blobdiff - pyderasn.py
Move generic decoder scheme and pretty printer out of main
[pyderasn.git] / pyderasn.py
index 5c311e09c11d2c39eed450a0d2f768bd195f1bea..717c06cce42d63a15ed38356f71eb92cce217076 100755 (executable)
@@ -135,6 +135,8 @@ example ``TBSCertificate`` sequence holds defaulted, explicitly tagged
 When default argument is used and value is not specified, then it equals
 to default one.
 
 When default argument is used and value is not specified, then it equals
 to default one.
 
+.. _bounds:
+
 Size constraints
 ________________
 
 Size constraints
 ________________
 
@@ -164,8 +166,10 @@ then :py:exc:`pyderasn.ObjNotReady` exception will be raised.
 All objects have ``copy()`` method, returning its copy, that can be safely
 mutated.
 
 All objects have ``copy()`` method, returning its copy, that can be safely
 mutated.
 
+.. _decoding:
+
 Decoding
 Decoding
-________
+--------
 
 Decoding is performed using ``decode()`` method. ``offset`` optional
 argument could be used to set initial object's offset in the binary
 
 Decoding is performed using ``decode()`` method. ``offset`` optional
 argument could be used to set initial object's offset in the binary
@@ -177,7 +181,7 @@ by specifying ``leavemm=True`` argument.
 When object is decoded, ``decoded`` property is true and you can safely
 use following properties:
 
 When object is decoded, ``decoded`` property is true and you can safely
 use following properties:
 
-* ``offset`` -- position from initial offset where object's tag is started
+* ``offset`` -- position including initial offset where object's tag starts
 * ``tlen`` -- length of object's tag
 * ``llen`` -- length of object's length value
 * ``vlen`` -- length of object's value
 * ``tlen`` -- length of object's tag
 * ``llen`` -- length of object's length value
 * ``vlen`` -- length of object's value
@@ -191,8 +195,10 @@ lesser than ``offset``), ``expl_tlen``, ``expl_llen``, ``expl_vlen``
 
 When error occurs, then :py:exc:`pyderasn.DecodeError` is raised.
 
 
 When error occurs, then :py:exc:`pyderasn.DecodeError` is raised.
 
+.. _pprinting:
+
 Pretty printing
 Pretty printing
-_______________
+---------------
 
 All objects have ``pps()`` method, that is a generator of
 :py:class:`pyderasn.PP` namedtuple, holding various raw information
 
 All objects have ``pps()`` method, that is a generator of
 :py:class:`pyderasn.PP` namedtuple, holding various raw information
@@ -209,6 +215,116 @@ all object ``repr``. But it is easy to write custom formatters.
     >>> print(pprint(obj))
         0   [1,1,   2] INTEGER -12345
 
     >>> print(pprint(obj))
         0   [1,1,   2] INTEGER -12345
 
+.. _definedby:
+
+DEFINED BY
+----------
+
+ASN.1 structures often have ANY and OCTET STRING fields, that are
+DEFINED BY some previously met ObjectIdentifier. This library provides
+ability to specify mapping between some OID and field that must be
+decoded with specific specification.
+
+defines kwarg
+_____________
+
+: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`)
+container::
+
+    class ContentInfo(Sequence):
+        schema = (
+            ("contentType", ContentType(defines=("content", {
+                id_digestedData: DigestedData(),
+                id_signedData: SignedData(),
+            }))),
+            ("content", Any(expl=tag_ctxc(0))),
+        )
+
+``contentType`` field tells that it defines that ``content`` must be
+decoded with ``SignedData`` specification, if ``contentType`` equals to
+``id-signedData``. The same applies to ``DigestedData``. If
+``contentType`` contains unknown OID, then no automatic decoding is
+done.
+
+Following types can be automatically decoded (DEFINED BY):
+
+* :py:class:`pyderasn.Any`
+* :py:class:`pyderasn.OctetString`
+* :py:class:`pyderasn.SequenceOf`/:py:class:`pyderasn.SetOf`
+  ``Any``/``OctetString``-s
+
+When any of those fields is automatically decoded, then ``.defined``
+attribute contains ``(OID, value)`` tuple. OID tell by which OID it was
+defined, ``value`` contains corresponding decoded value. For example
+above, ``content_info["content"].defined == (id_signedData,
+signed_data)``.
+
+defines_by_path kwarg
+_____________________
+
+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::
+
+    (decode_path, defines)
+
+where ``decode_path`` is a tuple holding so-called decode path to the
+exact :py:class:`pyderasn.ObjectIdentifier` field you want to apply
+``defines``, holding exactly the same value as accepted in its keyword
+argument.
+
+For example, again for CMS, you want to automatically decode
+``SignedData`` and CMC's (:rfc:`5272`) ``PKIData`` and ``PKIResponse``
+structures it may hold. Also, automatically decode ``controlSequence``
+of ``PKIResponse``::
+
+    content_info, tail = ContentInfo().decode(data, defines_by_path=(
+        (
+            ("contentType",),
+            ("content", {id_signedData: SignedData()}),
+        ),
+        (
+            (
+                "content",
+                decode_path_defby(id_signedData),
+                "encapContentInfo",
+                "eContentType",
+            ),
+            ("eContent", {
+                id_cct_PKIData: PKIData(),
+                id_cct_PKIResponse: PKIResponse(),
+            }),
+        ),
+        (
+            (
+                "content",
+                decode_path_defby(id_signedData),
+                "encapContentInfo",
+                "eContent",
+                decode_path_defby(id_cct_PKIResponse),
+                "controlSequence",
+                any,
+                "attrType",
+            ),
+            ("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``.
+First function is useful for path construction when some automatic
+decoding is already done. ``any`` is used for human readability and
+means literally any value it meet -- useful for sequence and set of-s.
+
 Primitive types
 ---------------
 
 Primitive types
 ---------------
 
@@ -338,6 +454,7 @@ __all__ = (
     "Boolean",
     "BoundsError",
     "Choice",
     "Boolean",
     "BoundsError",
     "Choice",
+    "decode_path_defby",
     "DecodeError",
     "Enumerated",
     "GeneralizedTime",
     "DecodeError",
     "Enumerated",
     "GeneralizedTime",
@@ -688,10 +805,7 @@ class Obj(object):
             optional=False,
             _decoded=(0, 0, 0),
     ):
             optional=False,
             _decoded=(0, 0, 0),
     ):
-        if impl is None:
-            self.tag = getattr(self, "impl", self.tag_default)
-        else:
-            self.tag = impl
+        self.tag = getattr(self, "impl", self.tag_default) if impl is None else impl
         self._expl = getattr(self, "expl", None) if expl is None else expl
         if self.tag != self.tag_default and self._expl is not None:
             raise ValueError(
         self._expl = getattr(self, "expl", None) if expl is None else expl
         if self.tag != self.tag_default and self._expl is not None:
             raise ValueError(
@@ -735,10 +849,22 @@ class Obj(object):
     def __str__(self):  # pragma: no cover
         return self.__bytes__() if PY2 else self.__unicode__()
 
     def __str__(self):  # pragma: no cover
         return self.__bytes__() if PY2 else self.__unicode__()
 
+    def __ne__(self, their):
+        return not(self == their)
+
+    def __gt__(self, their):  # pragma: no cover
+        return not(self < their)
+
+    def __le__(self, their):  # pragma: no cover
+        return (self == their) or (self < their)
+
+    def __ge__(self, their):  # pragma: no cover
+        return (self == their) or (self > their)
+
     def _encode(self):  # pragma: no cover
         raise NotImplementedError()
 
     def _encode(self):  # pragma: no cover
         raise NotImplementedError()
 
-    def _decode(self, tlv, offset=0, decode_path=()):  # pragma: no cover
+    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):  # pragma: no cover
         raise NotImplementedError()
 
     def encode(self):
         raise NotImplementedError()
 
     def encode(self):
@@ -747,13 +873,14 @@ class Obj(object):
             return raw
         return b"".join((self._expl, len_encode(len(raw)), raw))
 
             return raw
         return b"".join((self._expl, len_encode(len(raw)), raw))
 
-    def decode(self, data, offset=0, leavemm=False, decode_path=()):
+    def decode(self, data, offset=0, leavemm=False, decode_path=(), defines_by_path=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
         """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>`
         :returns: (Obj, remaining data)
         """
         tlv = memoryview(data)
         :returns: (Obj, remaining data)
         """
         tlv = memoryview(data)
@@ -762,6 +889,7 @@ class Obj(object):
                 tlv,
                 offset,
                 decode_path=decode_path,
                 tlv,
                 offset,
                 decode_path=decode_path,
+                defines_by_path=defines_by_path,
             )
         else:
             try:
             )
         else:
             try:
@@ -798,7 +926,8 @@ class Obj(object):
             obj, tail = self._decode(
                 v,
                 offset=offset + tlen + llen,
             obj, tail = self._decode(
                 v,
                 offset=offset + tlen + llen,
-                decode_path=(),
+                decode_path=decode_path,
+                defines_by_path=defines_by_path,
             )
         return obj, (tail if leavemm else tail.tobytes())
 
             )
         return obj, (tail if leavemm else tail.tobytes())
 
@@ -831,6 +960,12 @@ class Obj(object):
         return self.expl_tlen + self.expl_llen + self.expl_vlen
 
 
         return self.expl_tlen + self.expl_llen + self.expl_vlen
 
 
+def decode_path_defby(defined_by):
+    """DEFINED BY representation inside decode path
+    """
+    return "DEFINED BY (%s)" % defined_by
+
+
 ########################################################################
 # Pretty printing
 ########################################################################
 ########################################################################
 # Pretty printing
 ########################################################################
@@ -1100,7 +1235,7 @@ class Boolean(Obj):
             (b"\xFF" if self._value else b"\x00"),
         ))
 
             (b"\xFF" if self._value else b"\x00"),
         ))
 
-    def _decode(self, tlv, offset=0, decode_path=()):
+    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
         try:
             t, _, lv = tag_strip(tlv)
         except DecodeError as err:
         try:
             t, _, lv = tag_strip(tlv)
         except DecodeError as err:
@@ -1249,14 +1384,11 @@ class Integer(Obj):
         self._value = value
         specs = getattr(self, "schema", {}) if _specs is None else _specs
         self.specs = specs if isinstance(specs, dict) else dict(specs)
         self._value = value
         specs = getattr(self, "schema", {}) if _specs is None else _specs
         self.specs = specs if isinstance(specs, dict) else dict(specs)
-        if bounds is None:
-            self._bound_min, self._bound_max = getattr(
-                self,
-                "bounds",
-                (float("-inf"), float("+inf")),
-            )
-        else:
-            self._bound_min, self._bound_max = bounds
+        self._bound_min, self._bound_max = getattr(
+            self,
+            "bounds",
+            (float("-inf"), float("+inf")),
+        ) if bounds is None else bounds
         if value is not None:
             self._value = self._value_sanitize(value)
         if default is not None:
         if value is not None:
             self._value = self._value_sanitize(value)
         if default is not None:
@@ -1327,10 +1459,7 @@ class Integer(Obj):
         )
 
     def __lt__(self, their):
         )
 
     def __lt__(self, their):
-        return self._value < their
-
-    def __gt__(self, their):
-        return self._value > their
+        return self._value < their._value
 
     @property
     def named(self):
 
     @property
     def named(self):
@@ -1399,7 +1528,7 @@ class Integer(Obj):
                     break
         return b"".join((self.tag, len_encode(len(octets)), octets))
 
                     break
         return b"".join((self.tag, len_encode(len(octets)), octets))
 
-    def _decode(self, tlv, offset=0, decode_path=()):
+    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
         try:
             t, _, lv = tag_strip(tlv)
         except DecodeError as err:
         try:
             t, _, lv = tag_strip(tlv)
         except DecodeError as err:
@@ -1647,7 +1776,10 @@ class BitString(Obj):
 
     def copy(self):
         obj = self.__class__(_specs=self.specs)
 
     def copy(self):
         obj = self.__class__(_specs=self.specs)
-        obj._value = self._value
+        value = self._value
+        if value is not None:
+            value = (value[0], value[1])
+        obj._value = value
         obj.tag = self.tag
         obj._expl = self._expl
         obj.default = self.default
         obj.tag = self.tag
         obj._expl = self._expl
         obj.default = self.default
@@ -1729,7 +1861,7 @@ class BitString(Obj):
             octets,
         ))
 
             octets,
         ))
 
-    def _decode(self, tlv, offset=0, decode_path=()):
+    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
         try:
             t, _, lv = tag_strip(tlv)
         except DecodeError as err:
         try:
             t, _, lv = tag_strip(tlv)
         except DecodeError as err:
@@ -1850,7 +1982,7 @@ class OctetString(Obj):
     >>> OctetString(b"hell", bounds=(4, 4))
     OCTET STRING 4 bytes 68656c6c
     """
     >>> OctetString(b"hell", bounds=(4, 4))
     OCTET STRING 4 bytes 68656c6c
     """
-    __slots__ = ("_bound_min", "_bound_max")
+    __slots__ = ("_bound_min", "_bound_max", "defined")
     tag_default = tag_encode(4)
     asn1_type_name = "OCTET STRING"
 
     tag_default = tag_encode(4)
     asn1_type_name = "OCTET STRING"
 
@@ -1882,14 +2014,11 @@ class OctetString(Obj):
             _decoded,
         )
         self._value = value
             _decoded,
         )
         self._value = value
-        if bounds is None:
-            self._bound_min, self._bound_max = getattr(
-                self,
-                "bounds",
-                (0, float("+inf")),
-            )
-        else:
-            self._bound_min, self._bound_max = bounds
+        self._bound_min, self._bound_max = getattr(
+            self,
+            "bounds",
+            (0, float("+inf")),
+        ) if bounds is None else bounds
         if value is not None:
             self._value = self._value_sanitize(value)
         if default is not None:
         if value is not None:
             self._value = self._value_sanitize(value)
         if default is not None:
@@ -1901,6 +2030,7 @@ class OctetString(Obj):
             )
             if self._value is None:
                 self._value = default
             )
             if self._value is None:
                 self._value = default
+        self.defined = None
 
     def _value_sanitize(self, value):
         if issubclass(value.__class__, OctetString):
 
     def _value_sanitize(self, value):
         if issubclass(value.__class__, OctetString):
@@ -1946,6 +2076,9 @@ class OctetString(Obj):
             self._expl == their._expl
         )
 
             self._expl == their._expl
         )
 
+    def __lt__(self, their):
+        return self._value < their._value
+
     def __call__(
             self,
             value=None,
     def __call__(
             self,
             value=None,
@@ -1975,7 +2108,7 @@ class OctetString(Obj):
             self._value,
         ))
 
             self._value,
         ))
 
-    def _decode(self, tlv, offset=0, decode_path=()):
+    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
         try:
             t, _, lv = tag_strip(tlv)
         except DecodeError as err:
         try:
             t, _, lv = tag_strip(tlv)
         except DecodeError as err:
@@ -2050,6 +2183,11 @@ class OctetString(Obj):
             expl_llen=self.expl_llen if self.expled else None,
             expl_vlen=self.expl_vlen if self.expled else None,
         )
             expl_llen=self.expl_llen if self.expled else None,
             expl_vlen=self.expl_vlen if self.expled else None,
         )
+        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),)
+            )
 
 
 class Null(Obj):
 
 
 class Null(Obj):
@@ -2119,7 +2257,7 @@ class Null(Obj):
     def _encode(self):
         return self.tag + len_encode(0)
 
     def _encode(self):
         return self.tag + len_encode(0)
 
-    def _decode(self, tlv, offset=0, decode_path=()):
+    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
         try:
             t, _, lv = tag_strip(tlv)
         except DecodeError as err:
         try:
             t, _, lv = tag_strip(tlv)
         except DecodeError as err:
@@ -2199,13 +2337,14 @@ class ObjectIdentifier(Obj):
     Traceback (most recent call last):
     pyderasn.InvalidOID: unacceptable first arc value
     """
     Traceback (most recent call last):
     pyderasn.InvalidOID: unacceptable first arc value
     """
-    __slots__ = ()
+    __slots__ = ("defines",)
     tag_default = tag_encode(6)
     asn1_type_name = "OBJECT IDENTIFIER"
 
     def __init__(
             self,
             value=None,
     tag_default = tag_encode(6)
     asn1_type_name = "OBJECT IDENTIFIER"
 
     def __init__(
             self,
             value=None,
+            defines=None,
             impl=None,
             expl=None,
             default=None,
             impl=None,
             expl=None,
             default=None,
@@ -2216,6 +2355,13 @@ class ObjectIdentifier(Obj):
         :param value: set the value. Either tuples of integers,
                       string of "."-concatenated integers, or
                       :py:class:`pyderasn.ObjectIdentifier` object
         :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.
+                        :ref:`Read about DEFINED BY <definedby>`
         :param bytes impl: override default tag with ``IMPLICIT`` one
         :param bytes expl: override default tag with ``EXPLICIT`` one
         :param default: set default value. Type same as in ``value``
         :param bytes impl: override default tag with ``IMPLICIT`` one
         :param bytes expl: override default tag with ``EXPLICIT`` one
         :param default: set default value. Type same as in ``value``
@@ -2240,6 +2386,7 @@ class ObjectIdentifier(Obj):
             )
             if self._value is None:
                 self._value = default
             )
             if self._value is None:
                 self._value = default
+        self.defines = defines
 
     def __add__(self, their):
         if isinstance(their, self.__class__):
 
     def __add__(self, their):
         if isinstance(their, self.__class__):
@@ -2277,6 +2424,7 @@ class ObjectIdentifier(Obj):
     def copy(self):
         obj = self.__class__()
         obj._value = self._value
     def copy(self):
         obj = self.__class__()
         obj._value = self._value
+        obj.defines = self.defines
         obj.tag = self.tag
         obj._expl = self._expl
         obj.default = self.default
         obj.tag = self.tag
         obj._expl = self._expl
         obj.default = self.default
@@ -2313,14 +2461,12 @@ class ObjectIdentifier(Obj):
         )
 
     def __lt__(self, their):
         )
 
     def __lt__(self, their):
-        return self._value < their
-
-    def __gt__(self, their):
-        return self._value > their
+        return self._value < their._value
 
     def __call__(
             self,
             value=None,
 
     def __call__(
             self,
             value=None,
+            defines=None,
             impl=None,
             expl=None,
             default=None,
             impl=None,
             expl=None,
             default=None,
@@ -2328,6 +2474,7 @@ class ObjectIdentifier(Obj):
     ):
         return self.__class__(
             value=value,
     ):
         return self.__class__(
             value=value,
+            defines=self.defines if defines is None else defines,
             impl=self.tag if impl is None else impl,
             expl=self._expl if expl is None else expl,
             default=self.default if default is None else default,
             impl=self.tag if impl is None else impl,
             expl=self._expl if expl is None else expl,
             default=self.default if default is None else default,
@@ -2353,7 +2500,7 @@ class ObjectIdentifier(Obj):
         v = b"".join(octets)
         return b"".join((self.tag, len_encode(len(v)), v))
 
         v = b"".join(octets)
         return b"".join((self.tag, len_encode(len(v)), v))
 
-    def _decode(self, tlv, offset=0, decode_path=()):
+    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
         try:
             t, _, lv = tag_strip(tlv)
         except DecodeError as err:
         try:
             t, _, lv = tag_strip(tlv)
         except DecodeError as err:
@@ -2718,6 +2865,11 @@ class IA5String(CommonString):
     asn1_type_name = "IA5"
 
 
     asn1_type_name = "IA5"
 
 
+LEN_YYMMDDHHMMSSZ = len("YYMMDDHHMMSSZ")
+LEN_YYYYMMDDHHMMSSDMZ = len("YYYYMMDDHHMMSSDMZ")
+LEN_YYYYMMDDHHMMSSZ = len("YYYYMMDDHHMMSSZ")
+
+
 class UTCTime(CommonString):
     """``UTCTime`` datetime type
 
 class UTCTime(CommonString):
     """``UTCTime`` datetime type
 
@@ -2784,7 +2936,7 @@ class UTCTime(CommonString):
             return value.strftime(self.fmt).encode("ascii")
         if isinstance(value, binary_type):
             value_decoded = value.decode("ascii")
             return value.strftime(self.fmt).encode("ascii")
         if isinstance(value, binary_type):
             value_decoded = value.decode("ascii")
-            if len(value_decoded) == 2 + 2 + 2 + 2 + 2 + 2 + 1:
+            if len(value_decoded) == LEN_YYMMDDHHMMSSZ:
                 try:
                     datetime.strptime(value_decoded, self.fmt)
                 except ValueError:
                 try:
                     datetime.strptime(value_decoded, self.fmt)
                 except ValueError:
@@ -2875,7 +3027,7 @@ class GeneralizedTime(UTCTime):
             ).encode("ascii")
         if isinstance(value, binary_type):
             value_decoded = value.decode("ascii")
             ).encode("ascii")
         if isinstance(value, binary_type):
             value_decoded = value.decode("ascii")
-            if len(value_decoded) == 4 + 2 + 2 + 2 + 2 + 2 + 1:
+            if len(value_decoded) == LEN_YYYYMMDDHHMMSSZ:
                 try:
                     datetime.strptime(value_decoded, self.fmt)
                 except ValueError:
                 try:
                     datetime.strptime(value_decoded, self.fmt)
                 except ValueError:
@@ -2883,7 +3035,7 @@ class GeneralizedTime(UTCTime):
                         "invalid GeneralizedTime (without ms) format",
                     )
                 return value
                         "invalid GeneralizedTime (without ms) format",
                     )
                 return value
-            elif len(value_decoded) >= 4 + 2 + 2 + 2 + 2 + 2 + 1 + 1 + 1:
+            elif len(value_decoded) >= LEN_YYYYMMDDHHMMSSDMZ:
                 try:
                     datetime.strptime(value_decoded, self.fmt_ms)
                 except ValueError:
                 try:
                     datetime.strptime(value_decoded, self.fmt_ms)
                 except ValueError:
@@ -2900,7 +3052,7 @@ class GeneralizedTime(UTCTime):
 
     def todatetime(self):
         value = self._value.decode("ascii")
 
     def todatetime(self):
         value = self._value.decode("ascii")
-        if len(value) == 4 + 2 + 2 + 2 + 2 + 2 + 1:
+        if len(value) == LEN_YYYYMMDDHHMMSSZ:
             return datetime.strptime(value, self.fmt)
         return datetime.strptime(value, self.fmt_ms)
 
             return datetime.strptime(value, self.fmt)
         return datetime.strptime(value, self.fmt_ms)
 
@@ -3115,7 +3267,7 @@ class Choice(Obj):
         self._assert_ready()
         return self._value[1].encode()
 
         self._assert_ready()
         return self._value[1].encode()
 
-    def _decode(self, tlv, offset=0, decode_path=()):
+    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
         for choice, spec in self.specs.items():
             try:
                 value, tail = spec.decode(
         for choice, spec in self.specs.items():
             try:
                 value, tail = spec.decode(
@@ -3123,6 +3275,7 @@ class Choice(Obj):
                     offset=offset,
                     leavemm=True,
                     decode_path=decode_path + (choice,),
                     offset=offset,
                     leavemm=True,
                     decode_path=decode_path + (choice,),
+                    defines_by_path=defines_by_path,
                 )
             except TagMismatch:
                 continue
                 )
             except TagMismatch:
                 continue
@@ -3211,7 +3364,7 @@ class Any(Obj):
     >>> hexenc(bytes(a))
     b'0x040x0bhello world'
     """
     >>> hexenc(bytes(a))
     b'0x040x0bhello world'
     """
-    __slots__ = ()
+    __slots__ = ("defined",)
     tag_default = tag_encode(0)
     asn1_type_name = "ANY"
 
     tag_default = tag_encode(0)
     asn1_type_name = "ANY"
 
@@ -3232,6 +3385,7 @@ class Any(Obj):
         """
         super(Any, self).__init__(None, expl, None, optional, _decoded)
         self._value = None if value is None else self._value_sanitize(value)
         """
         super(Any, self).__init__(None, expl, None, optional, _decoded)
         self._value = None if value is None else self._value_sanitize(value)
+        self.defined = None
 
     def _value_sanitize(self, value):
         if isinstance(value, self.__class__):
 
     def _value_sanitize(self, value):
         if isinstance(value, self.__class__):
@@ -3288,7 +3442,7 @@ class Any(Obj):
         self._assert_ready()
         return self._value
 
         self._assert_ready()
         return self._value
 
-    def _decode(self, tlv, offset=0, decode_path=()):
+    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
         try:
             t, tlen, lv = tag_strip(tlv)
             l, llen, v = len_decode(lv)
         try:
             t, tlen, lv = tag_strip(tlv)
             l, llen, v = len_decode(lv)
@@ -3339,19 +3493,36 @@ class Any(Obj):
             expl_llen=self.expl_llen if self.expled else None,
             expl_vlen=self.expl_vlen if self.expled else None,
         )
             expl_llen=self.expl_llen if self.expled else None,
             expl_vlen=self.expl_vlen if self.expled else None,
         )
+        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),)
+            )
 
 
 ########################################################################
 # ASN.1 constructed types
 ########################################################################
 
 
 
 ########################################################################
 # ASN.1 constructed types
 ########################################################################
 
+def get_def_by_path(defines_by_path, sub_decode_path):
+    """Get define by decode path
+    """
+    for path, define in defines_by_path:
+        if len(path) != len(sub_decode_path):
+            continue
+        for p1, p2 in zip(path, sub_decode_path):
+            if (p1 != any) and (p1 != p2):
+                break
+        else:
+            return define
+
+
 class Sequence(Obj):
     """``SEQUENCE`` structure type
 
     You have to make specification of sequence::
 
         class Extension(Sequence):
 class Sequence(Obj):
     """``SEQUENCE`` structure type
 
     You have to make specification of sequence::
 
         class Extension(Sequence):
-            __slots__ = ()
             schema = (
                 ("extnID", ObjectIdentifier()),
                 ("critical", Boolean(default=False)),
             schema = (
                 ("extnID", ObjectIdentifier()),
                 ("critical", Boolean(default=False)),
@@ -3559,7 +3730,7 @@ class Sequence(Obj):
         v = b"".join(self._encoded_values())
         return b"".join((self.tag, len_encode(len(v)), v))
 
         v = b"".join(self._encoded_values())
         return b"".join((self.tag, len_encode(len(v)), v))
 
-    def _decode(self, tlv, offset=0, decode_path=()):
+    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
         try:
             t, tlen, lv = tag_strip(tlv)
         except DecodeError as err:
         try:
             t, tlen, lv = tag_strip(tlv)
         except DecodeError as err:
@@ -3594,27 +3765,81 @@ class Sequence(Obj):
         v, tail = v[:l], v[l:]
         sub_offset = offset + tlen + llen
         values = {}
         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
         for name, spec in self.specs.items():
             if len(v) == 0 and spec.optional:
                 continue
+            sub_decode_path = decode_path + (name,)
             try:
                 value, v_tail = spec.decode(
                     v,
                     sub_offset,
                     leavemm=True,
             try:
                 value, v_tail = spec.decode(
                     v,
                     sub_offset,
                     leavemm=True,
-                    decode_path=decode_path + (name,),
+                    decode_path=sub_decode_path,
+                    defines_by_path=defines_by_path,
                 )
             except TagMismatch:
                 if spec.optional:
                     continue
                 raise
                 )
             except TagMismatch:
                 if spec.optional:
                     continue
                 raise
+
+            defined = defines.pop(name, None)
+            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),
+                        )
+                        defined_value, defined_tail = defined_spec.decode(
+                            memoryview(bytes(_value)),
+                            sub_offset + value.tlen + value.llen,
+                            leavemm=True,
+                            decode_path=sub_sub_decode_path,
+                            defines_by_path=defines_by_path,
+                        )
+                        if len(defined_tail) > 0:
+                            raise DecodeError(
+                                "remaining data",
+                                klass=self.__class__,
+                                decode_path=sub_sub_decode_path,
+                                offset=offset,
+                            )
+                        _value.defined = (defined_by, defined_value)
+                else:
+                    defined_value, defined_tail = defined_spec.decode(
+                        memoryview(bytes(value)),
+                        sub_offset + value.tlen + value.llen,
+                        leavemm=True,
+                        decode_path=sub_decode_path + (decode_path_defby(defined_by),),
+                        defines_by_path=defines_by_path,
+                    )
+                    if len(defined_tail) > 0:
+                        raise DecodeError(
+                            "remaining data",
+                            klass=self.__class__,
+                            decode_path=sub_decode_path + (decode_path_defby(defined_by),),
+                            offset=offset,
+                        )
+                    value.defined = (defined_by, defined_value)
+
             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,
             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 still allow that
+                # but we allow that anyway
                 continue
             values[name] = value
                 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)
         if len(v) > 0:
             raise DecodeError(
                 "remaining data",
         if len(v) > 0:
             raise DecodeError(
                 "remaining data",
@@ -3683,7 +3908,7 @@ class Set(Sequence):
         v = b"".join(raws)
         return b"".join((self.tag, len_encode(len(v)), v))
 
         v = b"".join(raws)
         return b"".join((self.tag, len_encode(len(v)), v))
 
-    def _decode(self, tlv, offset=0, decode_path=()):
+    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
         try:
             t, tlen, lv = tag_strip(tlv)
         except DecodeError as err:
         try:
             t, tlen, lv = tag_strip(tlv)
         except DecodeError as err:
@@ -3726,6 +3951,7 @@ class Set(Sequence):
                         sub_offset,
                         leavemm=True,
                         decode_path=decode_path + (name,),
                         sub_offset,
                         leavemm=True,
                         decode_path=decode_path + (name,),
+                        defines_by_path=defines_by_path,
                     )
                 except TagMismatch:
                     continue
                     )
                 except TagMismatch:
                     continue
@@ -3812,14 +4038,11 @@ class SequenceOf(Obj):
         if schema is None:
             raise ValueError("schema must be specified")
         self.spec = schema
         if schema is None:
             raise ValueError("schema must be specified")
         self.spec = schema
-        if bounds is None:
-            self._bound_min, self._bound_max = getattr(
-                self,
-                "bounds",
-                (0, float("+inf")),
-            )
-        else:
-            self._bound_min, self._bound_max = bounds
+        self._bound_min, self._bound_max = getattr(
+            self,
+            "bounds",
+            (0, float("+inf")),
+        ) if bounds is None else bounds
         self._value = []
         if value is not None:
             self._value = self._value_sanitize(value)
         self._value = []
         if value is not None:
             self._value = self._value_sanitize(value)
@@ -3938,7 +4161,7 @@ class SequenceOf(Obj):
         v = b"".join(self._encoded_values())
         return b"".join((self.tag, len_encode(len(v)), v))
 
         v = b"".join(self._encoded_values())
         return b"".join((self.tag, len_encode(len(v)), v))
 
-    def _decode(self, tlv, offset=0, decode_path=()):
+    def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
         try:
             t, tlen, lv = tag_strip(tlv)
         except DecodeError as err:
         try:
             t, tlen, lv = tag_strip(tlv)
         except DecodeError as err:
@@ -3980,6 +4203,7 @@ class SequenceOf(Obj):
                 sub_offset,
                 leavemm=True,
                 decode_path=decode_path + (str(len(_value)),),
                 sub_offset,
                 leavemm=True,
                 decode_path=decode_path + (str(len(_value)),),
+                defines_by_path=defines_by_path,
             )
             sub_offset += (value.expl_tlvlen if value.expled else value.tlvlen)
             v = v_tail
             )
             sub_offset += (value.expl_tlvlen if value.expled else value.tlvlen)
             v = v_tail
@@ -4058,6 +4282,47 @@ def obj_by_path(pypath):  # pragma: no cover
     return obj
 
 
     return obj
 
 
+def generic_decoder():  # pragma: no cover
+    # All of this below is a big hack with self references
+    choice = PrimitiveTypes()
+    choice.specs["SequenceOf"] = SequenceOf(schema=choice)
+    choice.specs["SetOf"] = SetOf(schema=choice)
+    for i in range(31):
+        choice.specs["SequenceOf%d" % i] = SequenceOf(
+            schema=choice,
+            expl=tag_ctxc(i),
+        )
+    choice.specs["Any"] = Any()
+
+    # Class name equals to type name, to omit it from output
+    class SEQUENCEOF(SequenceOf):
+        __slots__ = ()
+        schema = choice
+
+    def pprint_any(obj, oids=None):
+        def _pprint_pps(pps):
+            for pp in pps:
+                if hasattr(pp, "_fields"):
+                    if pp.asn1_type_name == Choice.asn1_type_name:
+                        continue
+                    pp_kwargs = pp._asdict()
+                    pp_kwargs["decode_path"] = pp.decode_path[:-1] + (">",)
+                    pp = _pp(**pp_kwargs)
+                    yield pp_console_row(
+                        pp,
+                        oids=oids,
+                        with_offsets=True,
+                        with_blob=False,
+                    )
+                    for row in pp_console_blob(pp):
+                        yield row
+                else:
+                    for row in _pprint_pps(pp):
+                        yield row
+        return "\n".join(_pprint_pps(obj.pps()))
+    return SEQUENCEOF(), pprint_any
+
+
 def main():  # pragma: no cover
     import argparse
     parser = argparse.ArgumentParser(description="PyDERASN ASN.1 DER decoder")
 def main():  # pragma: no cover
     import argparse
     parser = argparse.ArgumentParser(description="PyDERASN ASN.1 DER decoder")
@@ -4083,45 +4348,7 @@ def main():  # pragma: no cover
         from functools import partial
         pprinter = partial(pprint, big_blobs=True)
     else:
         from functools import partial
         pprinter = partial(pprint, big_blobs=True)
     else:
-        # All of this below is a big hack with self references
-        choice = PrimitiveTypes()
-        choice.specs["SequenceOf"] = SequenceOf(schema=choice)
-        choice.specs["SetOf"] = SetOf(schema=choice)
-        for i in range(31):
-            choice.specs["SequenceOf%d" % i] = SequenceOf(
-                schema=choice,
-                expl=tag_ctxc(i),
-            )
-        choice.specs["Any"] = Any()
-
-        # Class name equals to type name, to omit it from output
-        class SEQUENCEOF(SequenceOf):
-            __slots__ = ()
-            schema = choice
-        schema = SEQUENCEOF()
-
-        def pprint_any(obj, oids=None):
-            def _pprint_pps(pps):
-                for pp in pps:
-                    if hasattr(pp, "_fields"):
-                        if pp.asn1_type_name == Choice.asn1_type_name:
-                            continue
-                        pp_kwargs = pp._asdict()
-                        pp_kwargs["decode_path"] = pp.decode_path[:-1] + (">",)
-                        pp = _pp(**pp_kwargs)
-                        yield pp_console_row(
-                            pp,
-                            oids=oids,
-                            with_offsets=True,
-                            with_blob=False,
-                        )
-                        for row in pp_console_blob(pp):
-                            yield row
-                    else:
-                        for row in _pprint_pps(pp):
-                            yield row
-            return "\n".join(_pprint_pps(obj.pps()))
-        pprinter = pprint_any
+        schema, pprinter = generic_decoder()
     obj, tail = schema().decode(der)
     print(pprinter(obj, oids=oids))
     if tail != b"":
     obj, tail = schema().decode(der)
     print(pprinter(obj, oids=oids))
     if tail != b"":