]> Cypherpunks.ru repositories - pyderasn.git/blobdiff - pyderasn.py
Move pretty-printing example to the main code
[pyderasn.git] / pyderasn.py
index 7d536f39b52bf9b7f149a0f98e0da149fd1a8d28..1c5b53f1f617810c1e8c3cb906710a282cf23126 100755 (executable)
@@ -239,6 +239,105 @@ all object ``repr``. But it is easy to write custom formatters.
     >>> print(pprint(obj))
         0   [1,1,   2] INTEGER -12345
 
+.. _pprint_example:
+
+Example certificate::
+
+    >>> print(pprint(crt))
+        0   [1,3,1604] Certificate SEQUENCE
+        4   [1,3,1453]  . tbsCertificate: TBSCertificate SEQUENCE
+       10-2 [1,1,   1]  . . version: [0] EXPLICIT Version INTEGER v3 OPTIONAL
+       13   [1,1,   3]  . . serialNumber: CertificateSerialNumber INTEGER 61595
+       18   [1,1,  13]  . . signature: AlgorithmIdentifier SEQUENCE
+       20   [1,1,   9]  . . . algorithm: OBJECT IDENTIFIER 1.2.840.113549.1.1.5
+       31   [0,0,   2]  . . . parameters: [UNIV 5] ANY OPTIONAL
+                        . . . . 05:00
+       33   [0,0, 278]  . . issuer: Name CHOICE rdnSequence
+       33   [1,3, 274]  . . . rdnSequence: RDNSequence SEQUENCE OF
+       37   [1,1,  11]  . . . . 0: RelativeDistinguishedName SET OF
+       39   [1,1,   9]  . . . . . 0: AttributeTypeAndValue SEQUENCE
+       41   [1,1,   3]  . . . . . . type: AttributeType OBJECT IDENTIFIER 2.5.4.6
+       46   [0,0,   4]  . . . . . . value: [UNIV 19] AttributeValue ANY
+                        . . . . . . . 13:02:45:53
+    [...]
+     1461   [1,1,  13]  . signatureAlgorithm: AlgorithmIdentifier SEQUENCE
+     1463   [1,1,   9]  . . algorithm: OBJECT IDENTIFIER 1.2.840.113549.1.1.5
+     1474   [0,0,   2]  . . parameters: [UNIV 5] ANY OPTIONAL
+                        . . . 05:00
+     1476   [1,2, 129]  . signatureValue: BIT STRING 1024 bits
+                        . . 68:EE:79:97:97:DD:3B:EF:16:6A:06:F2:14:9A:6E:CD
+                        . . 9E:12:F7:AA:83:10:BD:D1:7C:98:FA:C7:AE:D4:0E:2C
+     [...]
+
+    Trailing data: 0a
+
+Let's parse that output, human::
+
+       10-2 [1,1,   1]    . . version: [0] EXPLICIT Version INTEGER v3 OPTIONAL
+       ^  ^  ^ ^    ^     ^   ^        ^            ^       ^       ^  ^
+       0  1  2 3    4     5   6        7            8       9       10 11
+
+::
+
+       20   [1,1,   9]    . . . algorithm: OBJECT IDENTIFIER 1.2.840.113549.1.1.5
+       ^     ^ ^    ^     ^     ^          ^                 ^
+       0     2 3    4     5     6          9                 10
+
+::
+
+       33   [0,0, 278]    . . issuer: Name CHOICE rdnSequence
+       ^     ^ ^    ^     ^   ^       ^    ^      ^
+       0     2 3    4     5   6       8    9      10
+
+::
+
+       52-2∞ B [1,1,1054]∞  . . . . eContent: [0] EXPLICIT BER OCTET STRING 1046 bytes
+             ^           ^                                 ^   ^            ^
+            12          13                                14   9            10
+
+:0:
+ Offset of the object, where its DER/BER encoding begins.
+ Pay attention that it does **not** include explicit tag.
+:1:
+ If explicit tag exists, then this is its length (tag + encoded length).
+:2:
+ Length of object's tag. For example CHOICE does not have its own tag,
+ so it is zero.
+:3:
+ Length of encoded length.
+:4:
+ Length of encoded value.
+:5:
+ Visual indentation to show the depth of object in the hierarchy.
+:6:
+ Object's name inside SEQUENCE/CHOICE.
+:7:
+ If either IMPLICIT or EXPLICIT tag is set, then it will be shown
+ here. "IMPLICIT" is omitted.
+:8:
+ Object's class name, if set. Omitted if it is just an ordinary simple
+ value (like with ``algorithm`` in example above).
+:9:
+ Object's ASN.1 type.
+:10:
+ Object's value, if set. Can consist of multiple words (like OCTET/BIT
+ STRINGs above). We see ``v3`` value in Version, because it is named.
+ ``rdnSequence`` is the choice of CHOICE type.
+:11:
+ Possible other flags like OPTIONAL and DEFAULT, if value equals to the
+ default one, specified in the schema.
+:12:
+ Shows does object contains any kind of BER encoded data (possibly
+ Sequence holding BER-encoded underlying value).
+:13:
+ Only applicable to BER encoded data. Indefinite length encoding mark.
+:14:
+ Only applicable to BER encoded data. If object has BER-specific
+ encoding, then ``BER`` will be shown. It does not depend on indefinite
+ length encoding. ``EOC``, ``BOOLEAN``, ``BIT STRING``, ``OCTET STRING``
+ (and its derivatives), ``SET``, ``SET OF`` could be BERed.
+
+
 .. _definedby:
 
 DEFINED BY
@@ -463,6 +562,10 @@ NumericString
 _____________
 .. autoclass:: pyderasn.NumericString
 
+PrintableString
+_______________
+.. autoclass:: pyderasn.PrintableString
+
 UTCTime
 _______
 .. autoclass:: pyderasn.UTCTime
@@ -1551,10 +1654,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
@@ -1800,10 +1903,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:
@@ -2155,8 +2258,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
@@ -2197,6 +2298,8 @@ class BitString(Obj):
                 ("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
@@ -2605,10 +2708,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:
@@ -3579,7 +3682,7 @@ class NumericString(AllowableCharsMixin, CommonString):
     be stored.
 
     >>> NumericString().allowable_chars
-    set(['3', '4', '7', '5', '1', '0', '8', '9', ' ', '6', '2'])
+    frozenset(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' '])
     """
     __slots__ = ()
     tag_default = tag_encode(18)
@@ -3600,7 +3703,7 @@ class PrintableString(AllowableCharsMixin, CommonString):
     Its value is properly sanitized: see X.680 41.4 table 10.
 
     >>> PrintableString().allowable_chars
-    >>> set([' ', "'", ..., 'z'])
+    frozenset([' ', "'", ..., 'z'])
     """
     __slots__ = ()
     tag_default = tag_encode(19)
@@ -3661,14 +3764,16 @@ class UTCTime(CommonString):
     datetime.datetime(2017, 9, 30, 22, 7, 50)
     >>> UTCTime(datetime(2057, 9, 30, 22, 7, 50)).todatetime()
     datetime.datetime(1957, 9, 30, 22, 7, 50)
+
+    .. warning::
+
+       BER encoding is unsupported.
     """
     __slots__ = ()
     tag_default = tag_encode(23)
     encoding = "ascii"
     asn1_type_name = "UTCTime"
 
-    fmt = "%y%m%d%H%M%SZ"
-
     def __init__(
             self,
             value=None,
@@ -3707,24 +3812,36 @@ class UTCTime(CommonString):
             if self._value is None:
                 self._value = default
 
+    def _strptime(self, value):
+        # datetime.strptime's format: %y%m%d%H%M%SZ
+        if len(value) != LEN_YYMMDDHHMMSSZ:
+            raise ValueError("invalid UTCTime length")
+        if value[-1] != "Z":
+            raise ValueError("non UTC timezone")
+        return datetime(
+            2000 + int(value[:2]),  # %y
+            int(value[2:4]),  # %m
+            int(value[4:6]),  # %d
+            int(value[6:8]),  # %H
+            int(value[8:10]),  # %M
+            int(value[10:12]),  # %S
+        )
+
     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):
             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 (TypeError, ValueError):
-                    raise DecodeError("invalid UTCTime format")
-                return value
-            else:
-                raise DecodeError("invalid UTCTime length")
+            try:
+                self._strptime(value_decoded)
+            except (TypeError, ValueError) as err:
+                raise DecodeError("invalid UTCTime format: %r" % err)
+            return value
+        if isinstance(value, self.__class__):
+            return value._value
+        if isinstance(value, datetime):
+            return value.strftime("%y%m%d%H%M%SZ").encode("ascii")
         raise InvalidValueType((self.__class__, datetime))
 
     def __eq__(self, their):
@@ -3749,7 +3866,7 @@ class UTCTime(CommonString):
         having < 50 years are treated as 20xx, 19xx otherwise, according
         to X.509 recomendation.
         """
-        value = datetime.strptime(self._value.decode("ascii"), self.fmt)
+        value = self._strptime(self._value.decode("ascii"))
         year = value.year % 100
         return datetime(
             year=(2000 + year) if year < 50 else (1900 + year),
@@ -3801,54 +3918,85 @@ class GeneralizedTime(UTCTime):
     '20170930220750.000123Z'
     >>> t = GeneralizedTime(datetime(2057, 9, 30, 22, 7, 50))
     GeneralizedTime GeneralizedTime 2057-09-30T22:07:50
+
+    .. warning::
+
+       BER encoding is unsupported.
+
+    .. warning::
+
+       Only microsecond fractions are supported.
+       :py:exc:`pyderasn.DecodeError` will be raised during decoding of
+       higher precision values.
     """
     __slots__ = ()
     tag_default = tag_encode(24)
     asn1_type_name = "GeneralizedTime"
 
-    fmt = "%Y%m%d%H%M%SZ"
-    fmt_ms = "%Y%m%d%H%M%S.%fZ"
+    def _strptime(self, value):
+        l = len(value)
+        if l == LEN_YYYYMMDDHHMMSSZ:
+            # datetime.strptime's format: %y%m%d%H%M%SZ
+            if value[-1] != "Z":
+                raise ValueError("non UTC timezone")
+            return datetime(
+                int(value[:4]),  # %Y
+                int(value[4:6]),  # %m
+                int(value[6:8]),  # %d
+                int(value[8:10]),  # %H
+                int(value[10:12]),  # %M
+                int(value[12:14]),  # %S
+            )
+        if l >= LEN_YYYYMMDDHHMMSSDMZ:
+            # datetime.strptime's format: %Y%m%d%H%M%S.%fZ
+            if value[-1] != "Z":
+                raise ValueError("non UTC timezone")
+            if value[14] != ".":
+                raise ValueError("no fractions separator")
+            us = value[15:-1]
+            if us[-1] == "0":
+                raise ValueError("trailing zero")
+            us_len = len(us)
+            if us_len > 6:
+                raise ValueError("only microsecond fractions are supported")
+            us = int(us + ("0" * (6 - us_len)))
+            decoded = datetime(
+                int(value[:4]),  # %Y
+                int(value[4:6]),  # %m
+                int(value[6:8]),  # %d
+                int(value[8:10]),  # %H
+                int(value[10:12]),  # %M
+                int(value[12:14]),  # %S
+                us,  # %f
+            )
+            return decoded
+        raise ValueError("invalid GeneralizedTime length")
 
     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):
             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 (TypeError, ValueError):
-                    raise DecodeError(
-                        "invalid GeneralizedTime (without ms) format",
-                    )
-                return value
-            elif len(value_decoded) >= LEN_YYYYMMDDHHMMSSDMZ:
-                try:
-                    datetime.strptime(value_decoded, self.fmt_ms)
-                except (TypeError, ValueError):
-                    raise DecodeError(
-                        "invalid GeneralizedTime (with ms) format",
-                    )
-                return value
-            else:
+            try:
+                self._strptime(value_decoded)
+            except (TypeError, ValueError) as err:
                 raise DecodeError(
-                    "invalid GeneralizedTime length",
+                    "invalid GeneralizedTime format: %r" % err,
                     klass=self.__class__,
                 )
+            return value
+        if isinstance(value, self.__class__):
+            return value._value
+        if isinstance(value, datetime):
+            encoded = value.strftime("%Y%m%d%H%M%S")
+            if value.microsecond > 0:
+                encoded = encoded + (".%06d" % value.microsecond).rstrip("0")
+            return (encoded + "Z").encode("ascii")
         raise InvalidValueType((self.__class__, datetime))
 
     def todatetime(self):
-        value = self._value.decode("ascii")
-        if len(value) == LEN_YYYYMMDDHHMMSSZ:
-            return datetime.strptime(value, self.fmt)
-        return datetime.strptime(value, self.fmt_ms)
+        return self._strptime(self._value.decode("ascii"))
 
 
 class GraphicString(CommonString):
@@ -3967,8 +4115,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)
@@ -3977,6 +4123,8 @@ 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
@@ -4212,12 +4360,12 @@ 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