]> Cypherpunks.ru repositories - pyderasn.git/blobdiff - pyderasn.py
Remove msg kwarg for consistence
[pyderasn.git] / pyderasn.py
index b3ee318ae7f03cecbe120f8173e4de1a580e8eb6..278db2c496008082006cb4ed3e15ded0cb8482c0 100755 (executable)
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
 # coding: utf-8
-# PyDERASN -- Python ASN.1 DER codec with abstract structures
+# PyDERASN -- Python ASN.1 DER/BER codec with abstract structures
 # Copyright (C) 2017-2018 Sergey Matveev <stargrave@stargrave.org>
 #
 # This program is free software: you can redistribute it and/or modify
 # You should have received a copy of the GNU Lesser General Public
 # License along with this program.  If not, see
 # <http://www.gnu.org/licenses/>.
-"""Python ASN.1 DER codec with abstract structures
+"""Python ASN.1 DER/BER codec with abstract structures
 
-This library allows you to marshal and unmarshal various structures in
-ASN.1 DER format, like this:
+This library allows you to marshal various structures in ASN.1 DER
+format, unmarshal them in BER/CER/DER ones.
 
     >>> i = Integer(123)
     >>> raw = i.encode()
@@ -193,7 +193,7 @@ explicit tag. If you want to know information about it, then use:
 lesser than ``offset``), ``expl_tlen``, ``expl_llen``, ``expl_vlen``
 (that actually equals to ordinary ``tlvlen``).
 
-When error occurs, then :py:exc:`pyderasn.DecodeError` is raised.
+When error occurs, :py:exc:`pyderasn.DecodeError` is raised.
 
 .. _ctx:
 
@@ -206,6 +206,7 @@ decoding process.
 
 Currently available context options:
 
+* :ref:`bered <bered_ctx>`
 * :ref:`defines_by_path <defines_by_path_ctx>`
 * :ref:`strict_default_existence <strict_default_existence_ctx>`
 
@@ -363,6 +364,33 @@ 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.
 
+.. _bered_ctx:
+
+BER encoding
+------------
+
+.. warning::
+
+   Currently BER support is not extensively tested.
+
+By default PyDERASN accepts only DER encoded data. It always encodes to
+DER. But you can optionally enable BER decoding with setting ``bered``
+:ref:`context <ctx>` 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``
+  attribute is set to True. Only ``BOOLEAN``, ``BIT STRING``, ``OCTET
+  STRING`` 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.
+
+EOC (end-of-contents) token's length is taken in advance in object's
+value length.
+
 Primitive types
 ---------------
 
@@ -404,6 +432,10 @@ CommonString
 ____________
 .. autoclass:: pyderasn.CommonString
 
+NumericString
+_____________
+.. autoclass:: pyderasn.NumericString
+
 UTCTime
 _______
 .. autoclass:: pyderasn.UTCTime
@@ -464,6 +496,17 @@ Various
 .. autofunction:: pyderasn.tag_ctxp
 .. autofunction:: pyderasn.tag_ctxc
 .. autoclass:: pyderasn.Obj
+.. autoclass:: pyderasn.DecodeError
+   :members: __init__
+.. autoclass:: pyderasn.NotEnoughData
+.. autoclass:: pyderasn.LenIndefForm
+.. autoclass:: pyderasn.TagMismatch
+.. autoclass:: pyderasn.InvalidLength
+.. autoclass:: pyderasn.InvalidOID
+.. autoclass:: pyderasn.ObjUnknown
+.. autoclass:: pyderasn.ObjNotReady
+.. autoclass:: pyderasn.InvalidValueType
+.. autoclass:: pyderasn.BoundsError
 """
 
 from codecs import getdecoder
@@ -516,6 +559,7 @@ __all__ = (
     "InvalidOID",
     "InvalidValueType",
     "ISO646String",
+    "LenIndefForm",
     "NotEnoughData",
     "Null",
     "NumericString",
@@ -854,8 +898,8 @@ class Obj(object):
         "offset",
         "llen",
         "vlen",
-        "lenindef",
         "expl_lenindef",
+        "lenindef",
         "bered",
     )
 
@@ -870,16 +914,14 @@ class Obj(object):
         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(
-                "implicit and explicit tags can not be set simultaneously"
-            )
+            raise ValueError("implicit and explicit tags can not be set simultaneously")
         if default is not None:
             optional = True
         self.optional = optional
         self.offset, self.llen, self.vlen = _decoded
         self.default = None
-        self.lenindef = False
         self.expl_lenindef = False
+        self.lenindef = False
         self.bered = False
 
     @property
@@ -1014,7 +1056,7 @@ class Obj(object):
                 eoc_expected, tail = tail[:EOC_LEN], tail[EOC_LEN:]
                 if eoc_expected.tobytes() != EOC:
                     raise DecodeError(
-                        msg="no EOC",
+                        "no EOC",
                         decode_path=decode_path,
                         offset=offset,
                     )
@@ -1123,6 +1165,9 @@ PP = namedtuple("PP", (
     "expl_tlen",
     "expl_llen",
     "expl_vlen",
+    "expl_lenindef",
+    "lenindef",
+    "bered",
 ))
 
 
@@ -1144,6 +1189,9 @@ def _pp(
         expl_tlen=None,
         expl_llen=None,
         expl_vlen=None,
+        expl_lenindef=False,
+        lenindef=False,
+        bered=False,
 ):
     return PP(
         asn1_type_name,
@@ -1163,6 +1211,9 @@ def _pp(
         expl_tlen,
         expl_llen,
         expl_vlen,
+        expl_lenindef,
+        lenindef,
+        bered,
     )
 
 
@@ -1188,7 +1239,17 @@ def pp_console_row(
         )
         cols.append(_colorize(col, "red", with_colours, ()))
         col = "[%d,%d,%4d]" % (pp.tlen, pp.llen, pp.vlen)
-        cols.append(_colorize(col, "green", with_colours, ()))
+        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)
+        )
+        cols.append(col)
     if len(pp.decode_path) > 0:
         cols.append(" ." * (len(pp.decode_path)))
         ent = pp.decode_path[-1]
@@ -1216,6 +1277,8 @@ def pp_console_row(
         cols.append(_colorize(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))
     if pp.value is not None:
         value = pp.value
@@ -1239,7 +1302,7 @@ def pp_console_row(
 
 
 def pp_console_blob(pp):
-    cols = [" " * len("XXXXXYY [X,X,XXXX]")]
+    cols = [" " * len("XXXXXYY [X,X,XXXX]YY")]
     if len(pp.decode_path) > 0:
         cols.append(" ." * (len(pp.decode_path) + 1))
     if isinstance(pp.blob, binary_type):
@@ -1494,6 +1557,8 @@ class Boolean(Obj):
             expl_tlen=self.expl_tlen 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,
+            expl_lenindef=self.expl_lenindef,
+            bered=self.bered,
         )
 
 
@@ -1816,6 +1881,7 @@ class Integer(Obj):
             expl_tlen=self.expl_tlen 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,
+            expl_lenindef=self.expl_lenindef,
         )
 
 
@@ -1859,6 +1925,14 @@ class BitString(Obj):
     ['nonRepudiation', 'keyEncipherment']
     >>> b.specs
     {'nonRepudiation': 1, 'digitalSignature': 0, 'keyEncipherment': 2}
+
+    .. note::
+
+       Pay attention that BIT STRING can be encoded both in primitive
+       and constructed forms. Decoder always checks constructed form tag
+       additionally to specified primitive one. If BER decoding is
+       :ref:`not enabled <bered_ctx>`, then decoder will fail, because
+       of DER restrictions.
     """
     __slots__ = ("tag_constructed", "specs", "defined")
     tag_default = tag_encode(3)
@@ -2128,7 +2202,7 @@ class BitString(Obj):
         if t == self.tag_constructed:
             if not ctx.get("bered", False):
                 raise DecodeError(
-                    msg="unallowed BER constructed encoding",
+                    "unallowed BER constructed encoding",
                     decode_path=decode_path,
                     offset=offset,
                 )
@@ -2173,7 +2247,7 @@ class BitString(Obj):
                         break
                     if vlen > l:
                         raise DecodeError(
-                            msg="chunk out of bounds",
+                            "chunk out of bounds",
                             decode_path=len(chunks) - 1,
                             offset=chunks[-1].offset,
                         )
@@ -2188,7 +2262,7 @@ class BitString(Obj):
                     )
                 except TagMismatch:
                     raise DecodeError(
-                        msg="expected BitString encoded chunk",
+                        "expected BitString encoded chunk",
                         decode_path=sub_decode_path,
                         offset=sub_offset,
                     )
@@ -2198,7 +2272,7 @@ class BitString(Obj):
                 v = v_tail
             if len(chunks) == 0:
                 raise DecodeError(
-                    msg="no chunks",
+                    "no chunks",
                     decode_path=decode_path,
                     offset=offset,
                 )
@@ -2207,7 +2281,7 @@ class BitString(Obj):
             for chunk_i, chunk in enumerate(chunks[:-1]):
                 if chunk.bit_len % 8 != 0:
                     raise DecodeError(
-                        msg="BitString chunk is not multiple of 8 bit",
+                        "BitString chunk is not multiple of 8 bit",
                         decode_path=decode_path + (str(chunk_i),),
                         offset=chunk.offset,
                     )
@@ -2263,6 +2337,9 @@ class BitString(Obj):
             expl_tlen=self.expl_tlen 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,
+            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:
@@ -2286,6 +2363,14 @@ class OctetString(Obj):
     pyderasn.BoundsError: unsatisfied bounds: 4 <= 5 <= 4
     >>> OctetString(b"hell", bounds=(4, 4))
     OCTET STRING 4 bytes 68656c6c
+
+    .. note::
+
+       Pay attention that OCTET STRING can be encoded both in primitive
+       and constructed forms. Decoder always checks constructed form tag
+       additionally to specified primitive one. If BER decoding is
+       :ref:`not enabled <bered_ctx>`, then decoder will fail, because
+       of DER restrictions.
     """
     __slots__ = ("tag_constructed", "_bound_min", "_bound_max", "defined")
     tag_default = tag_encode(4)
@@ -2480,7 +2565,7 @@ class OctetString(Obj):
         if t == self.tag_constructed:
             if not ctx.get("bered", False):
                 raise DecodeError(
-                    msg="unallowed BER constructed encoding",
+                    "unallowed BER constructed encoding",
                     decode_path=decode_path,
                     offset=offset,
                 )
@@ -2525,7 +2610,7 @@ class OctetString(Obj):
                         break
                     if vlen > l:
                         raise DecodeError(
-                            msg="chunk out of bounds",
+                            "chunk out of bounds",
                             decode_path=len(chunks) - 1,
                             offset=chunks[-1].offset,
                         )
@@ -2540,7 +2625,7 @@ class OctetString(Obj):
                     )
                 except TagMismatch:
                     raise DecodeError(
-                        msg="expected OctetString encoded chunk",
+                        "expected OctetString encoded chunk",
                         decode_path=sub_decode_path,
                         offset=sub_offset,
                     )
@@ -2550,7 +2635,7 @@ class OctetString(Obj):
                 v = v_tail
             if len(chunks) == 0:
                 raise DecodeError(
-                    msg="no chunks",
+                    "no chunks",
                     decode_path=decode_path,
                     offset=offset,
                 )
@@ -2609,6 +2694,9 @@ class OctetString(Obj):
             expl_tlen=self.expl_tlen 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,
+            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:
@@ -2745,6 +2833,7 @@ class Null(Obj):
             expl_tlen=self.expl_tlen 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,
+            expl_lenindef=self.expl_lenindef,
         )
 
 
@@ -3033,6 +3122,7 @@ class ObjectIdentifier(Obj):
             expl_tlen=self.expl_tlen 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,
+            expl_lenindef=self.expl_lenindef,
         )
 
 
@@ -3255,6 +3345,7 @@ class CommonString(OctetString):
             expl_tlen=self.expl_tlen 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,
+            expl_lenindef=self.expl_lenindef,
         )
 
 
@@ -3266,6 +3357,10 @@ class UTF8String(CommonString):
 
 
 class NumericString(CommonString):
+    """Numeric string
+
+    Its value is properly sanitized: only ASCII digits can be stored.
+    """
     __slots__ = ()
     tag_default = tag_encode(18)
     encoding = "ascii"
@@ -3447,6 +3542,7 @@ class UTCTime(CommonString):
             expl_tlen=self.expl_tlen 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,
+            expl_lenindef=self.expl_lenindef,
         )
 
 
@@ -3778,6 +3874,7 @@ class Choice(Obj):
             tlen=self.tlen,
             llen=self.llen,
             vlen=self.vlen,
+            expl_lenindef=self.expl_lenindef,
         )
         if self.ready:
             yield self.value.pps(decode_path=decode_path + (self.choice,))
@@ -3998,6 +4095,8 @@ class Any(Obj):
             expl_tlen=self.expl_tlen 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,
+            expl_lenindef=self.expl_lenindef,
+            lenindef=self.lenindef,
         )
         defined_by, defined = self.defined or (None, None)
         if defined_by is not None:
@@ -4482,6 +4581,8 @@ class Sequence(Obj):
             expl_tlen=self.expl_tlen 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,
+            expl_lenindef=self.expl_lenindef,
+            lenindef=self.lenindef,
         )
         for name in self.specs:
             value = self._value.get(name)
@@ -4603,7 +4704,7 @@ class Set(Sequence):
         obj._value = values
         if not obj.ready:
             raise DecodeError(
-                msg="not all values are ready",
+                "not all values are ready",
                 klass=self.__class__,
                 decode_path=decode_path,
                 offset=offset,
@@ -4894,6 +4995,8 @@ class SequenceOf(Obj):
             expl_tlen=self.expl_tlen 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,
+            expl_lenindef=self.expl_lenindef,
+            lenindef=self.lenindef,
         )
         for i, value in enumerate(self._value):
             yield value.pps(decode_path=decode_path + (str(i),))
@@ -4977,7 +5080,7 @@ def generic_decoder():  # pragma: no cover
 
 def main():  # pragma: no cover
     import argparse
-    parser = argparse.ArgumentParser(description="PyDERASN ASN.1 DER decoder")
+    parser = argparse.ArgumentParser(description="PyDERASN ASN.1 BER/DER decoder")
     parser.add_argument(
         "--skip",
         type=int,
@@ -4996,6 +5099,11 @@ def main():  # pragma: no cover
         "--defines-by-path",
         help="Python path to decoder's defines_by_path",
     )
+    parser.add_argument(
+        "--nobered",
+        action='store_true',
+        help="Disallow BER encoding",
+    )
     parser.add_argument(
         "DERFile",
         type=argparse.FileType("rb"),
@@ -5012,7 +5120,7 @@ def main():  # pragma: no cover
         pprinter = partial(pprint, big_blobs=True)
     else:
         schema, pprinter = generic_decoder()
-    ctx = {"bered": True}
+    ctx = {"bered": not args.nobered}
     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)