#!/usr/bin/env python
# coding: utf-8
+# cython: language_level=3
# PyDERASN -- Python ASN.1 DER/BER codec with abstract structures
# Copyright (C) 2017-2020 Sergey Matveev <stargrave@stargrave.org>
#
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.
+ (and its derivatives), ``SET``, ``SET OF``, ``UTCTime``, ``GeneralizedTime``
+ could be BERed.
.. _definedby:
* If object is encoded in BER form (not the DER one), then ``ber_encoded``
attribute is set to True. Only ``BOOLEAN``, ``BIT STRING``, ``OCTET
- STRING``, ``OBJECT IDENTIFIER``, ``SEQUENCE``, ``SET``, ``SET OF``
- can contain it.
+ STRING``, ``OBJECT IDENTIFIER``, ``SEQUENCE``, ``SET``, ``SET OF``,
+ ``UTCTime``, ``GeneralizedTime`` 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
from collections import OrderedDict
from copy import copy
from datetime import datetime
+from datetime import timedelta
from math import ceil
from os import environ
from string import ascii_letters
from string import digits
+from sys import version_info
from unicodedata import category as unicat
from six import add_metaclass
def colored(what, *args, **kwargs):
return what
-__version__ = "6.0"
+__version__ = "6.3"
__all__ = (
"Any",
EOC_LEN = len(EOC)
LENINDEF = b"\x80" # length indefinite mark
LENINDEF_PP_CHAR = "I" if PY2 else "∞"
+NAMEDTUPLE_KWARGS = {} if version_info < (3, 6) else {"module": __name__}
+SET01 = frozenset("01")
+DECIMALS = frozenset(digits)
+DECIMAL_SIGNS = ".,"
+
+
+def pureint(value):
+ if not set(value) <= DECIMALS:
+ raise ValueError("non-pure integer")
+ return int(value)
+
+def fractions2float(fractions_raw):
+ pureint(fractions_raw)
+ return float("0." + fractions_raw)
########################################################################
"lenindef",
"ber_encoded",
"bered",
-))
+), **NAMEDTUPLE_KWARGS)
def _pp(
with_colours,
))
if with_blob:
- if isinstance(pp.blob, binary_type):
+ if pp.blob.__class__ == binary_type:
cols.append(hexenc(pp.blob))
- elif isinstance(pp.blob, tuple):
+ elif pp.blob.__class__ == tuple:
cols.append(", ".join(pp.blob))
if pp.optional:
cols.append(_colourize("OPTIONAL", "red", with_colours))
decode_path_len = len(pp.decode_path) - decode_path_len_decrease
if decode_path_len > 0:
cols.append(" ." * (decode_path_len + 1))
- if isinstance(pp.blob, binary_type):
+ if pp.blob.__class__ == binary_type:
blob = hexenc(pp.blob).upper()
for i in six_xrange(0, len(blob), 32):
chunk = blob[i:i + 32]
yield " ".join(cols + [colonize_hex(chunk)])
- elif isinstance(pp.blob, tuple):
+ elif pp.blob.__class__ == tuple:
yield " ".join(cols + [", ".join(pp.blob)])
"""Pretty print object
:param Obj obj: object you want to pretty print
- :param oid_maps: list of ``OID <-> humand readable string`` dictionary.
- When OID from it is met, then its humand readable form
- is printed
+ :param oid_maps: list of ``str(OID) <-> human readable string`` dictionary.
+ Its human readable form is printed when OID is met
:param big_blobs: if large binary objects are met (like OctetString
values), do we need to print them too, on separate
lines
"expl_lenindef",
"lenindef",
"ber_encoded",
-))
+), **NAMEDTUPLE_KWARGS)
class Boolean(Obj):
self._value = default
def _value_sanitize(self, value):
- if isinstance(value, bool):
+ if value.__class__ == bool:
return value
if issubclass(value.__class__, Boolean):
return value._value
return self._value
def __eq__(self, their):
- if isinstance(their, bool):
+ if their.__class__ == bool:
return self._value == their
if not issubclass(their.__class__, Boolean):
return False
"expl_lenindef",
"lenindef",
"ber_encoded",
-))
+), **NAMEDTUPLE_KWARGS)
class Integer(Obj):
super(Integer, self).__init__(impl, expl, default, optional, _decoded)
self._value = value
specs = getattr(self, "schema", {}) if _specs is None else _specs
- self.specs = specs if isinstance(specs, dict) else dict(specs)
+ self.specs = specs if specs.__class__ == dict else dict(specs)
self._bound_min, self._bound_max = getattr(
self,
"bounds",
pass
elif issubclass(value.__class__, Integer):
value = value._value
- elif isinstance(value, str):
+ elif value.__class__ == str:
value = self.specs.get(value)
if value is None:
raise ObjUnknown("integer value: %s" % value)
yield pp
-SET01 = frozenset(("0", "1"))
BitStringState = namedtuple("BitStringState", (
"version",
"specs",
"ber_encoded",
"tag_constructed",
"defined",
-))
+), **NAMEDTUPLE_KWARGS)
class BitString(Obj):
"""
super(BitString, self).__init__(impl, expl, default, optional, _decoded)
specs = getattr(self, "schema", {}) if _specs is None else _specs
- self.specs = specs if isinstance(specs, dict) else dict(specs)
+ self.specs = specs if specs.__class__ == dict else dict(specs)
self._value = None if value is None else self._value_sanitize(value)
if default is not None:
default = self._value_sanitize(default)
len(value) * 4,
hexdec(value + ("" if len(value) % 2 == 0 else "0")),
)
- if isinstance(value, binary_type):
+ if value.__class__ == binary_type:
return (len(value) * 8, value)
raise InvalidValueType((self.__class__, string_types, binary_type))
- if isinstance(value, tuple):
+ if value.__class__ == tuple:
if (
len(value) == 2 and
isinstance(value[0], integer_types) and
- isinstance(value[1], binary_type)
+ value[1].__class__ == binary_type
):
return value
bits = []
return self._value[1]
def __eq__(self, their):
- if isinstance(their, bytes):
+ if their.__class__ == bytes:
return self._value[1] == their
if not issubclass(their.__class__, BitString):
return False
)
def __getitem__(self, key):
- if isinstance(key, int):
+ if key.__class__ == int:
bit_len, octets = self._value
if key >= bit_len:
return False
octets,
))
- def _decode_chunk(self, lv, offset, decode_path):
- try:
- l, llen, v = len_decode(lv)
- except DecodeError as err:
- raise err.__class__(
- msg=err.msg,
- klass=self.__class__,
- decode_path=decode_path,
- offset=offset,
- )
- if l > len(v):
- raise NotEnoughData(
- "encoded length is longer than data",
- klass=self.__class__,
- decode_path=decode_path,
- offset=offset,
- )
- if l == 0:
- raise NotEnoughData(
- "zero length",
- klass=self.__class__,
- decode_path=decode_path,
- offset=offset,
- )
- pad_size = byte2int(v)
- if l == 1 and pad_size != 0:
- raise DecodeError(
- "invalid empty value",
- klass=self.__class__,
- decode_path=decode_path,
- offset=offset,
- )
- if pad_size > 7:
- raise DecodeError(
- "too big pad",
- klass=self.__class__,
- decode_path=decode_path,
- offset=offset,
- )
- if byte2int(v[l - 1:l]) & ((1 << pad_size) - 1) != 0:
- raise DecodeError(
- "invalid pad",
- klass=self.__class__,
- decode_path=decode_path,
- offset=offset,
- )
- v, tail = v[:l], v[l:]
- obj = self.__class__(
- value=((len(v) - 1) * 8 - pad_size, v[1:].tobytes()),
- impl=self.tag,
- expl=self._expl,
- default=self.default,
- optional=self.optional,
- _specs=self.specs,
- _decoded=(offset, llen, l),
- )
- return obj, tail
-
def _decode(self, tlv, offset, decode_path, ctx, tag_only):
try:
t, tlen, lv = tag_strip(tlv)
if t == self.tag:
if tag_only: # pragma: no cover
return None
- return self._decode_chunk(lv, offset, decode_path)
- if t == self.tag_constructed:
- if not ctx.get("bered", False):
- raise DecodeError(
- "unallowed BER constructed encoding",
- klass=self.__class__,
- decode_path=decode_path,
- offset=offset,
- )
- if tag_only: # pragma: no cover
- return None
- lenindef = False
try:
l, llen, v = len_decode(lv)
- except LenIndefForm:
- llen, l, v = 1, 0, lv[1:]
- lenindef = True
except DecodeError as err:
raise err.__class__(
msg=err.msg,
decode_path=decode_path,
offset=offset,
)
- if not lenindef and l == 0:
+ if l == 0:
raise NotEnoughData(
"zero length",
klass=self.__class__,
decode_path=decode_path,
offset=offset,
)
- chunks = []
- sub_offset = offset + tlen + llen
- vlen = 0
- while True:
- if lenindef:
- if v[:EOC_LEN].tobytes() == EOC:
- break
- else:
- if vlen == l:
- break
- if vlen > l:
- raise DecodeError(
- "chunk out of bounds",
- klass=self.__class__,
- decode_path=decode_path + (str(len(chunks) - 1),),
- offset=chunks[-1].offset,
- )
- sub_decode_path = decode_path + (str(len(chunks)),)
- try:
- chunk, v_tail = BitString().decode(
- v,
- offset=sub_offset,
- decode_path=sub_decode_path,
- leavemm=True,
- ctx=ctx,
- _ctx_immutable=False,
- )
- except TagMismatch:
- raise DecodeError(
- "expected BitString encoded chunk",
- klass=self.__class__,
- decode_path=sub_decode_path,
- offset=sub_offset,
- )
- chunks.append(chunk)
- sub_offset += chunk.tlvlen
- vlen += chunk.tlvlen
- v = v_tail
- if len(chunks) == 0:
+ pad_size = byte2int(v)
+ if l == 1 and pad_size != 0:
raise DecodeError(
- "no chunks",
+ "invalid empty value",
klass=self.__class__,
decode_path=decode_path,
offset=offset,
)
- values = []
- bit_len = 0
- for chunk_i, chunk in enumerate(chunks[:-1]):
- if chunk.bit_len % 8 != 0:
- raise DecodeError(
- "BitString chunk is not multiple of 8 bits",
- klass=self.__class__,
- decode_path=decode_path + (str(chunk_i),),
- offset=chunk.offset,
- )
- values.append(bytes(chunk))
- bit_len += chunk.bit_len
- chunk_last = chunks[-1]
- values.append(bytes(chunk_last))
- bit_len += chunk_last.bit_len
+ if pad_size > 7:
+ raise DecodeError(
+ "too big pad",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if byte2int(v[l - 1:l]) & ((1 << pad_size) - 1) != 0:
+ raise DecodeError(
+ "invalid pad",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ v, tail = v[:l], v[l:]
obj = self.__class__(
- value=(bit_len, b"".join(values)),
+ value=((len(v) - 1) * 8 - pad_size, v[1:].tobytes()),
impl=self.tag,
expl=self._expl,
default=self.default,
optional=self.optional,
_specs=self.specs,
- _decoded=(offset, llen, vlen + (EOC_LEN if lenindef else 0)),
+ _decoded=(offset, llen, l),
)
- obj.lenindef = lenindef
- obj.ber_encoded = True
- return obj, (v[EOC_LEN:] if lenindef else v)
- raise TagMismatch(
- klass=self.__class__,
- decode_path=decode_path,
- offset=offset,
+ return obj, tail
+ if t != self.tag_constructed:
+ raise TagMismatch(
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if not ctx.get("bered", False):
+ raise DecodeError(
+ "unallowed BER constructed encoding",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if tag_only: # pragma: no cover
+ return None
+ lenindef = False
+ try:
+ l, llen, v = len_decode(lv)
+ except LenIndefForm:
+ llen, l, v = 1, 0, lv[1:]
+ lenindef = True
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if l > len(v):
+ raise NotEnoughData(
+ "encoded length is longer than data",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if not lenindef and l == 0:
+ raise NotEnoughData(
+ "zero length",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ chunks = []
+ sub_offset = offset + tlen + llen
+ vlen = 0
+ while True:
+ if lenindef:
+ if v[:EOC_LEN].tobytes() == EOC:
+ break
+ else:
+ if vlen == l:
+ break
+ if vlen > l:
+ raise DecodeError(
+ "chunk out of bounds",
+ klass=self.__class__,
+ decode_path=decode_path + (str(len(chunks) - 1),),
+ offset=chunks[-1].offset,
+ )
+ sub_decode_path = decode_path + (str(len(chunks)),)
+ try:
+ chunk, v_tail = BitString().decode(
+ v,
+ offset=sub_offset,
+ decode_path=sub_decode_path,
+ leavemm=True,
+ ctx=ctx,
+ _ctx_immutable=False,
+ )
+ except TagMismatch:
+ raise DecodeError(
+ "expected BitString encoded chunk",
+ klass=self.__class__,
+ decode_path=sub_decode_path,
+ offset=sub_offset,
+ )
+ chunks.append(chunk)
+ sub_offset += chunk.tlvlen
+ vlen += chunk.tlvlen
+ v = v_tail
+ if len(chunks) == 0:
+ raise DecodeError(
+ "no chunks",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ values = []
+ bit_len = 0
+ for chunk_i, chunk in enumerate(chunks[:-1]):
+ if chunk.bit_len % 8 != 0:
+ raise DecodeError(
+ "BitString chunk is not multiple of 8 bits",
+ klass=self.__class__,
+ decode_path=decode_path + (str(chunk_i),),
+ offset=chunk.offset,
+ )
+ values.append(bytes(chunk))
+ bit_len += chunk.bit_len
+ chunk_last = chunks[-1]
+ values.append(bytes(chunk_last))
+ bit_len += chunk_last.bit_len
+ obj = self.__class__(
+ value=(bit_len, b"".join(values)),
+ impl=self.tag,
+ expl=self._expl,
+ default=self.default,
+ optional=self.optional,
+ _specs=self.specs,
+ _decoded=(offset, llen, vlen + (EOC_LEN if lenindef else 0)),
)
+ obj.lenindef = lenindef
+ obj.ber_encoded = True
+ return obj, (v[EOC_LEN:] if lenindef else v)
def __repr__(self):
return pp_console_row(next(self.pps()))
"ber_encoded",
"tag_constructed",
"defined",
-))
+), **NAMEDTUPLE_KWARGS)
class OctetString(Obj):
default=None,
optional=False,
_decoded=(0, 0, 0),
+ ctx=None,
):
"""
:param value: set the value. Either binary type, or
)
def _value_sanitize(self, value):
- if isinstance(value, binary_type):
+ if value.__class__ == binary_type:
pass
elif issubclass(value.__class__, OctetString):
value = value._value
return self._value
def __eq__(self, their):
- if isinstance(their, binary_type):
+ if their.__class__ == binary_type:
return self._value == their
if not issubclass(their.__class__, OctetString):
return False
self._value,
))
- def _decode_chunk(self, lv, offset, decode_path):
- try:
- l, llen, v = len_decode(lv)
- except DecodeError as err:
- raise err.__class__(
- msg=err.msg,
- klass=self.__class__,
- decode_path=decode_path,
- offset=offset,
- )
- if l > len(v):
- raise NotEnoughData(
- "encoded length is longer than data",
- klass=self.__class__,
- decode_path=decode_path,
- offset=offset,
- )
- v, tail = v[:l], v[l:]
- try:
- obj = self.__class__(
- value=v.tobytes(),
- bounds=(self._bound_min, self._bound_max),
- impl=self.tag,
- expl=self._expl,
- default=self.default,
- optional=self.optional,
- _decoded=(offset, llen, l),
- )
- except DecodeError as err:
- raise DecodeError(
- msg=err.msg,
- klass=self.__class__,
- decode_path=decode_path,
- offset=offset,
- )
- except BoundsError as err:
- raise DecodeError(
- msg=str(err),
- klass=self.__class__,
- decode_path=decode_path,
- offset=offset,
- )
- return obj, tail
-
def _decode(self, tlv, offset, decode_path, ctx, tag_only):
try:
t, tlen, lv = tag_strip(tlv)
if t == self.tag:
if tag_only:
return None
- return self._decode_chunk(lv, offset, decode_path)
- if t == self.tag_constructed:
- if not ctx.get("bered", False):
- raise DecodeError(
- "unallowed BER constructed encoding",
- klass=self.__class__,
- decode_path=decode_path,
- offset=offset,
- )
- if tag_only:
- return None
- lenindef = False
try:
l, llen, v = len_decode(lv)
- except LenIndefForm:
- llen, l, v = 1, 0, lv[1:]
- lenindef = True
except DecodeError as err:
raise err.__class__(
msg=err.msg,
decode_path=decode_path,
offset=offset,
)
- chunks = []
- sub_offset = offset + tlen + llen
- vlen = 0
- while True:
- if lenindef:
- if v[:EOC_LEN].tobytes() == EOC:
- break
- else:
- if vlen == l:
- break
- if vlen > l:
- raise DecodeError(
- "chunk out of bounds",
- klass=self.__class__,
- decode_path=decode_path + (str(len(chunks) - 1),),
- offset=chunks[-1].offset,
- )
- sub_decode_path = decode_path + (str(len(chunks)),)
- try:
- chunk, v_tail = OctetString().decode(
- v,
- offset=sub_offset,
- decode_path=sub_decode_path,
- leavemm=True,
- ctx=ctx,
- _ctx_immutable=False,
- )
- except TagMismatch:
- raise DecodeError(
- "expected OctetString encoded chunk",
- klass=self.__class__,
- decode_path=sub_decode_path,
- offset=sub_offset,
- )
- chunks.append(chunk)
- sub_offset += chunk.tlvlen
- vlen += chunk.tlvlen
- v = v_tail
+ v, tail = v[:l], v[l:]
try:
obj = self.__class__(
- value=b"".join(bytes(chunk) for chunk in chunks),
+ value=v.tobytes(),
bounds=(self._bound_min, self._bound_max),
impl=self.tag,
expl=self._expl,
default=self.default,
optional=self.optional,
- _decoded=(offset, llen, vlen + (EOC_LEN if lenindef else 0)),
+ _decoded=(offset, llen, l),
+ ctx=ctx,
)
except DecodeError as err:
raise DecodeError(
decode_path=decode_path,
offset=offset,
)
- obj.lenindef = lenindef
- obj.ber_encoded = True
- return obj, (v[EOC_LEN:] if lenindef else v)
- raise TagMismatch(
- klass=self.__class__,
- decode_path=decode_path,
- offset=offset,
- )
+ return obj, tail
+ if t != self.tag_constructed:
+ raise TagMismatch(
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if not ctx.get("bered", False):
+ raise DecodeError(
+ "unallowed BER constructed encoding",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if tag_only:
+ return None
+ lenindef = False
+ try:
+ l, llen, v = len_decode(lv)
+ except LenIndefForm:
+ llen, l, v = 1, 0, lv[1:]
+ lenindef = True
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if l > len(v):
+ raise NotEnoughData(
+ "encoded length is longer than data",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ chunks = []
+ sub_offset = offset + tlen + llen
+ vlen = 0
+ while True:
+ if lenindef:
+ if v[:EOC_LEN].tobytes() == EOC:
+ break
+ else:
+ if vlen == l:
+ break
+ if vlen > l:
+ raise DecodeError(
+ "chunk out of bounds",
+ klass=self.__class__,
+ decode_path=decode_path + (str(len(chunks) - 1),),
+ offset=chunks[-1].offset,
+ )
+ sub_decode_path = decode_path + (str(len(chunks)),)
+ try:
+ chunk, v_tail = OctetString().decode(
+ v,
+ offset=sub_offset,
+ decode_path=sub_decode_path,
+ leavemm=True,
+ ctx=ctx,
+ _ctx_immutable=False,
+ )
+ except TagMismatch:
+ raise DecodeError(
+ "expected OctetString encoded chunk",
+ klass=self.__class__,
+ decode_path=sub_decode_path,
+ offset=sub_offset,
+ )
+ chunks.append(chunk)
+ sub_offset += chunk.tlvlen
+ vlen += chunk.tlvlen
+ v = v_tail
+ try:
+ obj = self.__class__(
+ value=b"".join(bytes(chunk) for chunk in chunks),
+ bounds=(self._bound_min, self._bound_max),
+ impl=self.tag,
+ expl=self._expl,
+ default=self.default,
+ optional=self.optional,
+ _decoded=(offset, llen, vlen + (EOC_LEN if lenindef else 0)),
+ ctx=ctx,
+ )
+ except DecodeError as err:
+ raise DecodeError(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ except BoundsError as err:
+ raise DecodeError(
+ msg=str(err),
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ obj.lenindef = lenindef
+ obj.ber_encoded = True
+ return obj, (v[EOC_LEN:] if lenindef else v)
def __repr__(self):
return pp_console_row(next(self.pps()))
"expl_lenindef",
"lenindef",
"ber_encoded",
-))
+), **NAMEDTUPLE_KWARGS)
class Null(Obj):
"lenindef",
"ber_encoded",
"defines",
-))
+), **NAMEDTUPLE_KWARGS)
class ObjectIdentifier(Obj):
self.defines = defines
def __add__(self, their):
+ if their.__class__ == tuple:
+ return self.__class__(self._value + their)
if isinstance(their, self.__class__):
return self.__class__(self._value + their._value)
- if isinstance(their, tuple):
- return self.__class__(self._value + their)
raise InvalidValueType((self.__class__, tuple))
def _value_sanitize(self, value):
return value._value
if isinstance(value, string_types):
try:
- value = tuple(int(arc) for arc in value.split("."))
+ value = tuple(pureint(arc) for arc in value.split("."))
except ValueError:
raise InvalidOID("unacceptable arcs values")
- if isinstance(value, tuple):
+ if value.__class__ == tuple:
if len(value) < 2:
raise InvalidOID("less than 2 arcs")
first_arc = value[0]
pass
else:
raise InvalidOID("unacceptable first arc value")
+ if not all(arc >= 0 for arc in value):
+ raise InvalidOID("negative arc value")
return value
raise InvalidValueType((self.__class__, str, tuple))
)
def __eq__(self, their):
- if isinstance(their, tuple):
+ if their.__class__ == tuple:
return self._value == their
if not issubclass(their.__class__, ObjectIdentifier):
return False
def escape_control_unicode(c):
- if unicat(c).startswith("C"):
+ if unicat(c)[0] == "C":
c = repr(c).lstrip("u").strip("'")
return c
value_decoded = None
if isinstance(value, self.__class__):
value_raw = value._value
- elif isinstance(value, text_type):
+ elif value.__class__ == text_type:
value_decoded = value
- elif isinstance(value, binary_type):
+ elif value.__class__ == binary_type:
value_raw = value
else:
raise InvalidValueType((self.__class__, text_type, binary_type))
return value_raw
def __eq__(self, their):
- if isinstance(their, binary_type):
+ if their.__class__ == binary_type:
return self._value == their
- if isinstance(their, text_type):
+ if their.__class__ == text_type:
return self._value == their.encode(self.encoding)
if not isinstance(their, self.__class__):
return False
PrintableStringState = namedtuple(
"PrintableStringState",
OctetStringState._fields + ("allowable_chars",),
+ **NAMEDTUPLE_KWARGS
)
default=None,
optional=False,
_decoded=(0, 0, 0),
+ ctx=None,
allow_asterisk=False,
allow_ampersand=False,
):
if allow_ampersand:
self._allowable_chars |= self._ampersand
super(PrintableString, self).__init__(
- value, bounds, impl, expl, default, optional, _decoded,
+ value, bounds, impl, expl, default, optional, _decoded, ctx,
)
@property
LEN_YYYYMMDDHHMMSSZ = len("YYYYMMDDHHMMSSZ")
-class UTCTime(CommonString):
+class VisibleString(CommonString):
+ __slots__ = ()
+ tag_default = tag_encode(26)
+ encoding = "ascii"
+ asn1_type_name = "VisibleString"
+
+
+UTCTimeState = namedtuple(
+ "UTCTimeState",
+ OctetStringState._fields + ("ber_raw",),
+ **NAMEDTUPLE_KWARGS
+)
+
+
+def str_to_time_fractions(value):
+ v = pureint(value)
+ year, v = (v // 10**10), (v % 10**10)
+ month, v = (v // 10**8), (v % 10**8)
+ day, v = (v // 10**6), (v % 10**6)
+ hour, v = (v // 10**4), (v % 10**4)
+ minute, second = (v // 100), (v % 100)
+ return year, month, day, hour, minute, second
+
+
+class UTCTime(VisibleString):
"""``UTCTime`` datetime type
>>> t = UTCTime(datetime(2017, 9, 30, 22, 7, 50, 123))
>>> UTCTime(datetime(2057, 9, 30, 22, 7, 50)).todatetime()
datetime.datetime(1957, 9, 30, 22, 7, 50)
+ If BER encoded value was met, then ``ber_raw`` attribute will hold
+ its raw representation.
+
.. warning::
- BER encoding is unsupported.
+ Pay attention that UTCTime can not hold full year, so all years
+ having < 50 years are treated as 20xx, 19xx otherwise, according
+ to X.509 recommendation.
+
+ .. warning::
+
+ No strict validation of UTC offsets are made, but very crude:
+
+ * minutes are not exceeding 60
+ * offset value is not exceeding 14 hours
"""
- __slots__ = ()
+ __slots__ = ("ber_raw",)
tag_default = tag_encode(23)
encoding = "ascii"
asn1_type_name = "UTCTime"
optional=False,
_decoded=(0, 0, 0),
bounds=None, # dummy argument, workability for OctetString.decode
+ ctx=None,
):
"""
:param value: set the value. Either datetime type, or
:param bool optional: is object ``OPTIONAL`` in sequence
"""
super(UTCTime, self).__init__(
- None, None, impl, expl, default, optional, _decoded,
+ None, None, impl, expl, None, optional, _decoded, ctx,
)
self._value = value
+ self.ber_raw = None
if value is not None:
- self._value = self._value_sanitize(value)
+ self._value, self.ber_raw = self._value_sanitize(value, ctx)
+ self.ber_encoded = self.ber_raw is not None
if default is not None:
- default = self._value_sanitize(default)
+ default, _ = self._value_sanitize(default)
self.default = self.__class__(
value=default,
impl=self.tag,
)
if self._value is None:
self._value = default
+ optional = True
+ self.optional = optional
+
+ def _strptime_bered(self, value):
+ year, month, day, hour, minute, _ = str_to_time_fractions(value[:10] + "00")
+ value = value[10:]
+ if len(value) == 0:
+ raise ValueError("no timezone")
+ year += 2000 if year < 50 else 1900
+ decoded = datetime(year, month, day, hour, minute)
+ offset = 0
+ if value[-1] == "Z":
+ value = value[:-1]
+ else:
+ if len(value) < 5:
+ raise ValueError("invalid UTC offset")
+ if value[-5] == "-":
+ sign = -1
+ elif value[-5] == "+":
+ sign = 1
+ else:
+ raise ValueError("invalid UTC offset")
+ v = pureint(value[-4:])
+ offset, v = (60 * (v % 100)), v // 100
+ if offset >= 3600:
+ raise ValueError("invalid UTC offset minutes")
+ offset += 3600 * v
+ if offset > 14 * 3600:
+ raise ValueError("too big UTC offset")
+ offset *= sign
+ value = value[:-5]
+ if len(value) == 0:
+ return offset, decoded
+ if len(value) != 2:
+ raise ValueError("invalid UTC offset seconds")
+ seconds = pureint(value)
+ if seconds >= 60:
+ raise ValueError("invalid seconds value")
+ return offset, decoded + timedelta(seconds=seconds)
def _strptime(self, value):
# datetime.strptime's format: %y%m%d%H%M%SZ
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
- )
+ year, month, day, hour, minute, second = str_to_time_fractions(value[:-1])
+ year += 2000 if year < 50 else 1900
+ return datetime(year, month, day, hour, minute, second)
- def _value_sanitize(self, value):
- if isinstance(value, binary_type):
+ def _dt_sanitize(self, value):
+ if value.year < 1950 or value.year > 2049:
+ raise ValueError("UTCTime can hold only 1950-2049 years")
+ return value.replace(microsecond=0)
+
+ def _value_sanitize(self, value, ctx=None):
+ if value.__class__ == binary_type:
try:
value_decoded = value.decode("ascii")
except (UnicodeEncodeError, UnicodeDecodeError) as err:
raise DecodeError("invalid UTCTime encoding: %r" % err)
+ err = None
try:
- self._strptime(value_decoded)
- except (TypeError, ValueError) as err:
- raise DecodeError("invalid UTCTime format: %r" % err)
- return value
+ return self._strptime(value_decoded), None
+ except (TypeError, ValueError) as _err:
+ err = _err
+ if (ctx is not None) and ctx.get("bered", False):
+ try:
+ offset, _value = self._strptime_bered(value_decoded)
+ _value = _value - timedelta(seconds=offset)
+ return self._dt_sanitize(_value), value
+ except (TypeError, ValueError, OverflowError) as _err:
+ err = _err
+ raise DecodeError(
+ "invalid %s format: %r" % (self.asn1_type_name, err),
+ klass=self.__class__,
+ )
if isinstance(value, self.__class__):
- return value._value
- if isinstance(value, datetime):
- return value.strftime("%y%m%d%H%M%SZ").encode("ascii")
+ return value._value, None
+ if value.__class__ == datetime:
+ return self._dt_sanitize(value), None
raise InvalidValueType((self.__class__, datetime))
+ def _pp_value(self):
+ if self.ready:
+ value = self._value.isoformat()
+ if self.ber_encoded:
+ value += " (%s)" % self.ber_raw
+ return value
+
+ def __unicode__(self):
+ if self.ready:
+ value = self._value.isoformat()
+ if self.ber_encoded:
+ value += " (%s)" % self.ber_raw
+ return value
+ return text_type(self._pp_value())
+
+ def __getstate__(self):
+ return UTCTimeState(
+ *super(UTCTime, self).__getstate__(),
+ **{"ber_raw": self.ber_raw}
+ )
+
+ def __setstate__(self, state):
+ super(UTCTime, self).__setstate__(state)
+ self.ber_raw = state.ber_raw
+
+ def __bytes__(self):
+ self._assert_ready()
+ return self._encode_time()
+
def __eq__(self, their):
- if isinstance(their, binary_type):
- return self._value == their
- if isinstance(their, datetime):
+ if their.__class__ == binary_type:
+ return self._encode_time() == their
+ if their.__class__ == datetime:
return self.todatetime() == their
if not isinstance(their, self.__class__):
return False
self._expl == their._expl
)
- def todatetime(self):
- """Convert to datetime
+ def _encode_time(self):
+ return self._value.strftime("%y%m%d%H%M%SZ").encode("ascii")
- :returns: datetime
+ def _encode(self):
+ self._assert_ready()
+ value = self._encode_time()
+ return b"".join((self.tag, len_encode(len(value)), value))
- Pay attention that UTCTime can not hold full year, so all years
- having < 50 years are treated as 20xx, 19xx otherwise, according
- to X.509 recomendation.
- """
- value = self._strptime(self._value.decode("ascii"))
- year = value.year % 100
- return datetime(
- year=(2000 + year) if year < 50 else (1900 + year),
- month=value.month,
- day=value.day,
- hour=value.hour,
- minute=value.minute,
- second=value.second,
- )
+ def todatetime(self):
+ return self._value
def __repr__(self):
return pp_console_row(next(self.pps()))
asn1_type_name=self.asn1_type_name,
obj_name=self.__class__.__name__,
decode_path=decode_path,
- value=self.todatetime().isoformat() if self.ready else None,
+ value=self._pp_value(),
optional=self.optional,
default=self == self.default,
impl=None if self.tag == self.tag_default else tag_decode(self.tag),
.. warning::
- BER encoding is unsupported.
+ Only microsecond fractions are supported in DER encoding.
+ :py:exc:`pyderasn.DecodeError` will be raised during decoding of
+ higher precision values.
.. warning::
- Only microsecond fractions are supported.
- :py:exc:`pyderasn.DecodeError` will be raised during decoding of
- higher precision values.
+ BER encoded data can loss information (accuracy) during decoding
+ because of float transformations.
+
+ .. warning::
+
+ Local times (without explicit timezone specification) are treated
+ as UTC one, no transformations are made.
.. warning::
tag_default = tag_encode(24)
asn1_type_name = "GeneralizedTime"
+ def _dt_sanitize(self, value):
+ return value
+
+ def _strptime_bered(self, value):
+ if len(value) < 4 + 3 * 2:
+ raise ValueError("invalid GeneralizedTime")
+ year, month, day, hour, _, _ = str_to_time_fractions(value[:10] + "0000")
+ decoded = datetime(year, month, day, hour)
+ offset, value = 0, value[10:]
+ if len(value) == 0:
+ return offset, decoded
+ if value[-1] == "Z":
+ value = value[:-1]
+ else:
+ for char, sign in (("-", -1), ("+", 1)):
+ idx = value.rfind(char)
+ if idx == -1:
+ continue
+ offset_raw, value = value[idx + 1:].replace(":", ""), value[:idx]
+ v = pureint(offset_raw)
+ if len(offset_raw) == 4:
+ offset, v = (60 * (v % 100)), v // 100
+ if offset >= 3600:
+ raise ValueError("invalid UTC offset minutes")
+ elif len(offset_raw) == 2:
+ pass
+ else:
+ raise ValueError("invalid UTC offset")
+ offset += 3600 * v
+ if offset > 14 * 3600:
+ raise ValueError("too big UTC offset")
+ offset *= sign
+ break
+ if len(value) == 0:
+ return offset, decoded
+ if value[0] in DECIMAL_SIGNS:
+ return offset, (
+ decoded + timedelta(seconds=3600 * fractions2float(value[1:]))
+ )
+ if len(value) < 2:
+ raise ValueError("stripped minutes")
+ decoded += timedelta(seconds=60 * pureint(value[:2]))
+ value = value[2:]
+ if len(value) == 0:
+ return offset, decoded
+ if value[0] in DECIMAL_SIGNS:
+ return offset, (
+ decoded + timedelta(seconds=60 * fractions2float(value[1:]))
+ )
+ if len(value) < 2:
+ raise ValueError("stripped seconds")
+ decoded += timedelta(seconds=pureint(value[:2]))
+ value = value[2:]
+ if len(value) == 0:
+ return offset, decoded
+ if value[0] not in DECIMAL_SIGNS:
+ raise ValueError("invalid format after seconds")
+ return offset, (
+ decoded + timedelta(microseconds=10**6 * fractions2float(value[1:]))
+ )
+
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
- )
+ return datetime(*str_to_time_fractions(value[:-1]))
if l >= LEN_YYYYMMDDHHMMSSDMZ:
# datetime.strptime's format: %Y%m%d%H%M%S.%fZ
if value[-1] != "Z":
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
+ us = pureint(us + ("0" * (6 - us_len)))
+ year, month, day, hour, minute, second = str_to_time_fractions(value[:14])
+ return datetime(year, month, day, hour, minute, second, us)
raise ValueError("invalid GeneralizedTime length")
- def _value_sanitize(self, value):
- if isinstance(value, binary_type):
- try:
- value_decoded = value.decode("ascii")
- except (UnicodeEncodeError, UnicodeDecodeError) as err:
- raise DecodeError("invalid GeneralizedTime encoding: %r" % err)
- try:
- self._strptime(value_decoded)
- except (TypeError, ValueError) as err:
- raise DecodeError(
- "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):
- return self._strptime(self._value.decode("ascii"))
+ def _encode_time(self):
+ value = self._value
+ encoded = value.strftime("%Y%m%d%H%M%S")
+ if value.microsecond > 0:
+ encoded += (".%06d" % value.microsecond).rstrip("0")
+ return (encoded + "Z").encode("ascii")
class GraphicString(CommonString):
asn1_type_name = "GraphicString"
-class VisibleString(CommonString):
- __slots__ = ()
- tag_default = tag_encode(26)
- encoding = "ascii"
- asn1_type_name = "VisibleString"
-
-
class ISO646String(VisibleString):
__slots__ = ()
asn1_type_name = "ISO646String"
"expl_lenindef",
"lenindef",
"ber_encoded",
-))
+), **NAMEDTUPLE_KWARGS)
class Choice(Obj):
if len(schema) == 0:
raise ValueError("schema must be specified")
self.specs = (
- schema if isinstance(schema, OrderedDict) else OrderedDict(schema)
+ schema if schema.__class__ == OrderedDict else OrderedDict(schema)
)
self._value = None
if value is not None:
self._value = copy(default_obj._value)
def _value_sanitize(self, value):
- if isinstance(value, tuple) and len(value) == 2:
+ if (value.__class__ == tuple) and len(value) == 2:
choice, obj = value
spec = self.specs.get(choice)
if spec is None:
self.ber_encoded = state.ber_encoded
def __eq__(self, their):
- if isinstance(their, tuple) and len(their) == 2:
+ if (their.__class__ == tuple) and len(their) == 2:
return self._value == their
if not isinstance(their, self.__class__):
return False
"lenindef",
"ber_encoded",
"defined",
-))
+), **NAMEDTUPLE_KWARGS)
class Any(Obj):
self.defined = None
def _value_sanitize(self, value):
- if isinstance(value, binary_type):
+ if value.__class__ == binary_type:
return value
if isinstance(value, self.__class__):
return value._value
self.defined = state.defined
def __eq__(self, their):
- if isinstance(their, binary_type):
+ if their.__class__ == binary_type:
return self._value == their
if issubclass(their.__class__, Any):
return self._value == their._value
"expl_lenindef",
"lenindef",
"ber_encoded",
-))
+), **NAMEDTUPLE_KWARGS)
class Sequence(Obj):
if schema is None:
schema = getattr(self, "schema", ())
self.specs = (
- schema if isinstance(schema, OrderedDict) else OrderedDict(schema)
+ schema if schema.__class__ == OrderedDict else OrderedDict(schema)
)
self._value = {}
if value is not None:
tail = v[EOC_LEN:]
obj.lenindef = True
obj._value = values
- if not obj.ready:
- raise DecodeError(
- "not all values are ready",
- klass=self.__class__,
- decode_path=decode_path,
- offset=offset,
- )
+ for name, spec in iteritems(self.specs):
+ if name not in values and not spec.optional:
+ raise DecodeError(
+ "%s value is not ready" % name,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
obj.ber_encoded = ber_encoded
return obj, tail
"expl_lenindef",
"lenindef",
"ber_encoded",
-))
+), **NAMEDTUPLE_KWARGS)
class SequenceOf(Obj):