#!/usr/bin/env python
# coding: utf-8
# PyDERASN -- Python ASN.1 DER/BER codec with abstract structures
-# Copyright (C) 2017-2018 Sergey Matveev <stargrave@stargrave.org>
+# Copyright (C) 2017-2019 Sergey Matveev <stargrave@stargrave.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
+# published by the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
>>> 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
structures it may hold. Also, automatically decode ``controlSequence``
of ``PKIResponse``::
- content_info, tail = ContentInfo().decode(data, defines_by_path=(
+ content_info, tail = ContentInfo().decode(data, ctx={"defines_by_path": (
(
("contentType",),
((("content",), {id_signedData: SignedData()}),),
id_cmc_transactionId: TransactionId(),
})),
),
- ))
+ )})
Pay attention for :py:class:`pyderasn.DecodePathDefBy` and ``any``.
First function is useful for path construction when some automatic
: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``
+* 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``, ``SEQUENCE``, ``SET``, ``SET OF`` can contain it.
+ STRING``, ``OBJECT IDENTIFIER``, ``SEQUENCE``, ``SET``, ``SET OF``
+ 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.
+* If object has either any of BER-related encoding (explicit tag
+ indefinite length, object's indefinite length, BER-encoding) or any
+ underlying component has that kind of encoding, then ``bered``
+ attribute is set to True. For example SignedData CMS can have
+ ``ContentInfo:content:signerInfos:*`` ``bered`` value set to True, but
+ ``ContentInfo:content:signerInfos:*:signedAttrs`` won't.
EOC (end-of-contents) token's length is taken in advance in object's
value length.
_____________
.. autoclass:: pyderasn.NumericString
+PrintableString
+_______________
+.. autoclass:: pyderasn.PrintableString
+
UTCTime
_______
.. autoclass:: pyderasn.UTCTime
-------
.. autofunction:: pyderasn.abs_decode_path
+.. autofunction:: pyderasn.colonize_hex
.. autofunction:: pyderasn.hexenc
.. autofunction:: pyderasn.hexdec
.. autofunction:: pyderasn.tag_encode
from codecs import getencoder
from collections import namedtuple
from collections import OrderedDict
+from copy import copy
from datetime import datetime
from math import ceil
from os import environ
+from string import ascii_letters
from string import digits
from six import add_metaclass
from six import int2byte
from six import integer_types
from six import iterbytes
+from six import iteritems
+from six import itervalues
from six import PY2
from six import string_types
from six import text_type
+from six import unichr as six_unichr
from six.moves import xrange as six_xrange
try:
from termcolor import colored
-except ImportError:
- def colored(what, *args):
+except ImportError: # pragma: no cover
+ def colored(what, *args, **kwargs):
return what
# Errors
########################################################################
-class DecodeError(Exception):
+class ASN1Error(ValueError):
+ pass
+
+
+class DecodeError(ASN1Error):
def __init__(self, msg="", klass=None, decode_path=(), offset=0):
"""
:param str msg: reason of decode failing
pass
-class ObjUnknown(ValueError):
+class ObjUnknown(ASN1Error):
def __init__(self, name):
super(ObjUnknown, self).__init__()
self.name = name
return "%s(%s)" % (self.__class__.__name__, self)
-class ObjNotReady(ValueError):
+class ObjNotReady(ASN1Error):
def __init__(self, name):
super(ObjNotReady, self).__init__()
self.name = name
return "%s(%s)" % (self.__class__.__name__, self)
-class InvalidValueType(ValueError):
+class InvalidValueType(ASN1Error):
def __init__(self, expected_types):
super(InvalidValueType, self).__init__()
self.expected_types = expected_types
return "%s(%s)" % (self.__class__.__name__, self)
-class BoundsError(ValueError):
+class BoundsError(ASN1Error):
def __init__(self, bound_min, value, bound_max):
super(BoundsError, self).__init__()
self.bound_min = bound_min
"vlen",
"expl_lenindef",
"lenindef",
- "bered",
+ "ber_encoded",
)
def __init__(
self.default = None
self.expl_lenindef = False
self.lenindef = False
- self.bered = False
+ self.ber_encoded = False
@property
def ready(self): # pragma: no cover
if not self.ready:
raise ObjNotReady(self.__class__.__name__)
+ @property
+ def bered(self):
+ """Is either object or any elements inside is BER encoded?
+ """
+ return self.expl_lenindef or self.lenindef or self.ber_encoded
+
@property
def decoded(self):
"""Is object decoded?
decode_path=(),
ctx=None,
tag_only=False,
+ _ctx_immutable=True,
):
"""Decode the data
: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 ctx: optional :ref:`context <ctx>` governing decoding process.
+ :param ctx: optional :ref:`context <ctx>` governing decoding process
:param tag_only: decode only the tag, without length and contents
(used only in Choice and Set structures, trying to
determine if tag satisfies the scheme)
+ :param _ctx_immutable: do we need to copy ``ctx`` before using it
:returns: (Obj, remaining data)
"""
if ctx is None:
ctx = {}
+ elif _ctx_immutable:
+ ctx = copy(ctx)
tlv = memoryview(data)
if self._expl is None:
result = self._decode(
ctx=ctx,
tag_only=tag_only,
)
- if tag_only:
+ if tag_only: # pragma: no cover
return
obj, tail = result
eoc_expected, tail = tail[:EOC_LEN], tail[EOC_LEN:]
ctx=ctx,
tag_only=tag_only,
)
- if tag_only:
+ if tag_only: # pragma: no cover
return
obj, tail = result
if obj.tlvlen < l and not ctx.get("allow_expl_oob", False):
return self.expl_tlvlen if self.expled else self.tlvlen
def pps_lenindef(self, decode_path):
- if self.lenindef:
+ if self.lenindef and not (
+ getattr(self, "defined", None) is not None and
+ self.defined[1].lenindef
+ ):
yield _pp(
asn1_type_name="EOC",
obj_name="",
tlen=1,
llen=1,
vlen=0,
+ ber_encoded=True,
bered=True,
)
if self.expl_lenindef:
tlen=1,
llen=1,
vlen=0,
+ ber_encoded=True,
bered=True,
)
########################################################################
PP = namedtuple("PP", (
+ "obj",
"asn1_type_name",
"obj_name",
"decode_path",
"expl_vlen",
"expl_lenindef",
"lenindef",
+ "ber_encoded",
"bered",
))
def _pp(
+ obj=None,
asn1_type_name="unknown",
obj_name="unknown",
decode_path=(),
expl_vlen=None,
expl_lenindef=False,
lenindef=False,
+ ber_encoded=False,
bered=False,
):
return PP(
+ obj,
asn1_type_name,
obj_name,
decode_path,
expl_vlen,
expl_lenindef,
lenindef,
+ ber_encoded,
bered,
)
return colored(what, colour, attrs=attrs) if with_colours else what
+def colonize_hex(hexed):
+ """Separate hexadecimal string with colons
+ """
+ return ":".join(hexed[i:i + 2] for i in six_xrange(0, len(hexed), 2))
+
+
def pp_console_row(
pp,
- oids=None,
+ oid_maps=(),
with_offsets=False,
with_blob=True,
with_colours=False,
),
LENINDEF_PP_CHAR if pp.expl_lenindef else " ",
)
- cols.append(_colourize(col, "red", with_colours, ()))
+ col = _colourize(col, "red", with_colours, ())
+ col += _colourize("B", "red", with_colours) if pp.bered else " "
+ cols.append(col)
col = "[%d,%d,%4d]%s" % (
pp.tlen,
pp.llen,
if isinstance(ent, DecodePathDefBy):
cols.append(_colourize("DEFINED BY", "red", with_colours, ("reverse",)))
value = str(ent.defined_by)
+ oid_name = None
if (
- oids is not None and
+ len(oid_maps) > 0 and
ent.defined_by.asn1_type_name ==
- ObjectIdentifier.asn1_type_name and
- value in oids
+ ObjectIdentifier.asn1_type_name
):
- cols.append(_colourize("%s:" % oids[value], "green", with_colours))
- else:
+ for oid_map in oid_maps:
+ oid_name = oid_map.get(value)
+ if oid_name is not None:
+ cols.append(_colourize("%s:" % oid_name, "green", with_colours))
+ break
+ if oid_name is None:
cols.append(_colourize("%s:" % value, "white", with_colours, ("reverse",)))
else:
cols.append(_colourize("%s:" % ent, "yellow", with_colours, ("reverse",)))
cols.append(_colourize(col, "blue", with_colours))
if pp.asn1_type_name.replace(" ", "") != pp.obj_name.upper():
cols.append(_colourize(pp.obj_name, "magenta", with_colours))
- if pp.bered:
+ if pp.ber_encoded:
cols.append(_colourize("BER", "red", with_colours))
cols.append(_colourize(pp.asn1_type_name, "cyan", with_colours))
if pp.value is not None:
value = pp.value
cols.append(_colourize(value, "white", with_colours, ("reverse",)))
if (
- oids is not None and
- pp.asn1_type_name == ObjectIdentifier.asn1_type_name and
- value in oids
+ len(oid_maps) > 0 and
+ pp.asn1_type_name == ObjectIdentifier.asn1_type_name
):
- cols.append(_colourize("(%s)" % oids[value], "green", with_colours))
+ for oid_map in oid_maps:
+ oid_name = oid_map.get(value)
+ if oid_name is not None:
+ cols.append(_colourize("(%s)" % oid_name, "green", with_colours))
+ break
+ if pp.asn1_type_name == Integer.asn1_type_name:
+ hex_repr = hex(int(pp.obj._value))[2:].upper()
+ if len(hex_repr) % 2 != 0:
+ hex_repr = "0" + hex_repr
+ cols.append(_colourize(
+ "(%s)" % colonize_hex(hex_repr),
+ "green",
+ with_colours,
+ ))
if with_blob:
if isinstance(pp.blob, binary_type):
cols.append(hexenc(pp.blob))
def pp_console_blob(pp, decode_path_len_decrease=0):
- cols = [" " * len("XXXXXYYZ [X,X,XXXX]Z")]
+ cols = [" " * len("XXXXXYYZZ [X,X,XXXX]Z")]
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):
blob = hexenc(pp.blob).upper()
- for i in range(0, len(blob), 32):
+ for i in six_xrange(0, len(blob), 32):
chunk = blob[i:i + 32]
- yield " ".join(cols + [":".join(
- chunk[j:j + 2] for j in range(0, len(chunk), 2)
- )])
+ yield " ".join(cols + [colonize_hex(chunk)])
elif isinstance(pp.blob, tuple):
yield " ".join(cols + [", ".join(pp.blob)])
def pprint(
obj,
- oids=None,
+ oid_maps=(),
big_blobs=False,
with_colours=False,
with_decode_path=False,
"""Pretty print object
:param Obj obj: object you want to pretty print
- :param oids: ``OID <-> humand readable string`` dictionary. When OID
- from it is met, then its humand readable form is printed
+ :param oid_maps: list of ``OID <-> humand readable string`` dictionary.
+ When OID from it is met, then its humand readable form
+ is printed
:param big_blobs: if large binary objects are met (like OctetString
values), do we need to print them too, on separate
lines
if big_blobs:
yield pp_console_row(
pp,
- oids=oids,
+ oid_maps=oid_maps,
with_offsets=True,
with_blob=False,
with_colours=with_colours,
else:
yield pp_console_row(
pp,
- oids=oids,
+ oid_maps=oid_maps,
with_offsets=True,
with_blob=True,
with_colours=with_colours,
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
obj.offset = self.offset
obj.llen = self.llen
obj.vlen = self.vlen
+ obj.expl_lenindef = self.expl_lenindef
+ obj.lenindef = self.lenindef
+ obj.ber_encoded = self.ber_encoded
return obj
def __nonzero__(self):
offset=offset,
)
first_octet = byte2int(v)
- bered = False
+ ber_encoded = False
if first_octet == 0:
value = False
elif first_octet == 0xFF:
value = True
elif ctx.get("bered", False):
value = True
- bered = True
+ ber_encoded = True
else:
raise DecodeError(
"unacceptable Boolean value",
optional=self.optional,
_decoded=(offset, 1, 1),
)
- obj.bered = bered
+ obj.ber_encoded = ber_encoded
return obj, v[1:]
def __repr__(self):
def pps(self, decode_path=()):
yield _pp(
+ obj=self,
asn1_type_name=self.asn1_type_name,
obj_name=self.__class__.__name__,
decode_path=decode_path,
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,
+ ber_encoded=self.ber_encoded,
bered=self.bered,
)
for pp in self.pps_lenindef(decode_path):
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:
obj.offset = self.offset
obj.llen = self.llen
obj.vlen = self.vlen
+ obj.expl_lenindef = self.expl_lenindef
+ obj.lenindef = self.lenindef
+ obj.ber_encoded = self.ber_encoded
return obj
def __int__(self):
@property
def named(self):
- for name, value in self.specs.items():
+ for name, value in iteritems(self.specs):
if value == self._value:
return name
def pps(self, decode_path=()):
yield _pp(
+ obj=self,
asn1_type_name=self.asn1_type_name,
obj_name=self.__class__.__name__,
decode_path=decode_path,
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,
)
for pp in self.pps_lenindef(decode_path):
yield pp
+SET01 = frozenset(("0", "1"))
+
+
class BitString(Obj):
"""``BIT STRING`` bit string type
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
):
if value.endswith("'B"):
value = value[1:-2]
- if not set(value) <= set(("0", "1")):
+ if not frozenset(value) <= SET01:
raise ValueError("B's coding contains unacceptable chars")
return self._bits2octets(value)
elif value.endswith("'H"):
bits.append(bit)
if len(bits) == 0:
return self._bits2octets("")
- bits = set(bits)
+ bits = frozenset(bits)
return self._bits2octets("".join(
("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
obj.offset = self.offset
obj.llen = self.llen
obj.vlen = self.vlen
+ obj.expl_lenindef = self.expl_lenindef
+ obj.lenindef = self.lenindef
+ obj.ber_encoded = self.ber_encoded
return obj
def __iter__(self):
@property
def named(self):
- return [name for name, bit in self.specs.items() if self[bit]]
+ return [name for name, bit in iteritems(self.specs) if self[bit]]
def __call__(
self,
offset=offset,
)
if t == self.tag:
- if tag_only:
+ if tag_only: # pragma: no cover
return
return self._decode_chunk(lv, offset, decode_path, ctx)
if t == self.tag_constructed:
decode_path=decode_path,
offset=offset,
)
- if tag_only:
+ if tag_only: # pragma: no cover
return
lenindef = False
try:
decode_path=sub_decode_path,
leavemm=True,
ctx=ctx,
+ _ctx_immutable=False,
)
except TagMismatch:
raise DecodeError(
_decoded=(offset, llen, vlen + (EOC_LEN if lenindef else 0)),
)
obj.lenindef = lenindef
- obj.bered = True
+ obj.ber_encoded = True
return obj, (v[EOC_LEN:] if lenindef else v)
raise TagMismatch(
klass=self.__class__,
if len(self.specs) > 0:
blob = tuple(self.named)
yield _pp(
+ obj=self,
asn1_type_name=self.asn1_type_name,
obj_name=self.__class__.__name__,
decode_path=decode_path,
expl_vlen=self.expl_vlen if self.expled else None,
expl_lenindef=self.expl_lenindef,
lenindef=self.lenindef,
+ ber_encoded=self.ber_encoded,
bered=self.bered,
)
defined_by, defined = self.defined or (None, None)
)
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:
obj.offset = self.offset
obj.llen = self.llen
obj.vlen = self.vlen
+ obj.expl_lenindef = self.expl_lenindef
+ obj.lenindef = self.lenindef
+ obj.ber_encoded = self.ber_encoded
return obj
def __bytes__(self):
decode_path=sub_decode_path,
leavemm=True,
ctx=ctx,
+ _ctx_immutable=False,
)
except TagMismatch:
raise DecodeError(
offset=offset,
)
obj.lenindef = lenindef
- obj.bered = True
+ obj.ber_encoded = True
return obj, (v[EOC_LEN:] if lenindef else v)
raise TagMismatch(
klass=self.__class__,
def pps(self, decode_path=()):
yield _pp(
+ obj=self,
asn1_type_name=self.asn1_type_name,
obj_name=self.__class__.__name__,
decode_path=decode_path,
expl_vlen=self.expl_vlen if self.expled else None,
expl_lenindef=self.expl_lenindef,
lenindef=self.lenindef,
+ ber_encoded=self.ber_encoded,
bered=self.bered,
)
defined_by, defined = self.defined or (None, None)
obj.offset = self.offset
obj.llen = self.llen
obj.vlen = self.vlen
+ obj.expl_lenindef = self.expl_lenindef
+ obj.lenindef = self.lenindef
+ obj.ber_encoded = self.ber_encoded
return obj
def __eq__(self, their):
decode_path=decode_path,
offset=offset,
)
- if tag_only:
+ if tag_only: # pragma: no cover
return
try:
l, _, v = len_decode(lv)
def pps(self, decode_path=()):
yield _pp(
+ obj=self,
asn1_type_name=self.asn1_type_name,
obj_name=self.__class__.__name__,
decode_path=decode_path,
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,
)
for pp in self.pps_lenindef(decode_path):
yield pp
obj.offset = self.offset
obj.llen = self.llen
obj.vlen = self.vlen
+ obj.expl_lenindef = self.expl_lenindef
+ obj.lenindef = self.lenindef
+ obj.ber_encoded = self.ber_encoded
return obj
def __iter__(self):
decode_path=decode_path,
offset=offset,
)
- if tag_only:
+ if tag_only: # pragma: no cover
return
try:
l, llen, v = len_decode(lv)
)
v, tail = v[:l], v[l:]
arcs = []
+ ber_encoded = False
while len(v) > 0:
i = 0
arc = 0
while True:
octet = indexbytes(v, i)
+ if i == 0 and octet == 0x80:
+ if ctx.get("bered", False):
+ ber_encoded = True
+ else:
+ raise DecodeError("non normalized arc encoding")
arc = (arc << 7) | (octet & 0x7F)
if octet & 0x80 == 0:
arcs.append(arc)
optional=self.optional,
_decoded=(offset, llen, l),
)
+ if ber_encoded:
+ obj.ber_encoded = True
return obj, tail
def __repr__(self):
def pps(self, decode_path=()):
yield _pp(
+ obj=self,
asn1_type_name=self.asn1_type_name,
obj_name=self.__class__.__name__,
decode_path=decode_path,
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,
+ ber_encoded=self.ber_encoded,
+ bered=self.bered,
)
for pp in self.pps_lenindef(decode_path):
yield pp
if isinstance(value, self.__class__):
value = value._value
elif isinstance(value, integer_types):
- if value not in list(self.specs.values()):
+ for _value in itervalues(self.specs):
+ if _value == value:
+ break
+ else:
raise DecodeError(
"unknown integer value: %s" % value,
klass=self.__class__,
obj.offset = self.offset
obj.llen = self.llen
obj.vlen = self.vlen
+ obj.expl_lenindef = self.expl_lenindef
+ obj.lenindef = self.lenindef
+ obj.ber_encoded = self.ber_encoded
return obj
def __call__(
if self.ready:
value = hexenc(bytes(self)) if no_unicode else self.__unicode__()
yield _pp(
+ obj=self,
asn1_type_name=self.asn1_type_name,
obj_name=self.__class__.__name__,
decode_path=decode_path,
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,
+ ber_encoded=self.ber_encoded,
+ bered=self.bered,
)
for pp in self.pps_lenindef(decode_path):
yield pp
asn1_type_name = "UTF8String"
-class NumericString(CommonString):
+class AllowableCharsMixin(object):
+ @property
+ def allowable_chars(self):
+ if PY2:
+ return self._allowable_chars
+ return frozenset(six_unichr(c) for c in self._allowable_chars)
+
+
+class NumericString(AllowableCharsMixin, CommonString):
"""Numeric string
- Its value is properly sanitized: only ASCII digits can be stored.
+ Its value is properly sanitized: only ASCII digits with spaces can
+ be stored.
+
+ >>> NumericString().allowable_chars
+ frozenset(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' '])
"""
__slots__ = ()
tag_default = tag_encode(18)
encoding = "ascii"
asn1_type_name = "NumericString"
- allowable_chars = set(digits.encode("ascii"))
+ _allowable_chars = frozenset(digits.encode("ascii") + b" ")
def _value_sanitize(self, value):
value = super(NumericString, self)._value_sanitize(value)
- if not set(value) <= self.allowable_chars:
+ if not frozenset(value) <= self._allowable_chars:
raise DecodeError("non-numeric value")
return value
-class PrintableString(CommonString):
+class PrintableString(AllowableCharsMixin, CommonString):
+ """Printable string
+
+ Its value is properly sanitized: see X.680 41.4 table 10.
+
+ >>> PrintableString().allowable_chars
+ frozenset([' ', "'", ..., 'z'])
+ """
__slots__ = ()
tag_default = tag_encode(19)
encoding = "ascii"
asn1_type_name = "PrintableString"
+ _allowable_chars = frozenset(
+ (ascii_letters + digits + " '()+,-./:=?").encode("ascii")
+ )
+
+ def _value_sanitize(self, value):
+ value = super(PrintableString, self)._value_sanitize(value)
+ if not frozenset(value) <= self._allowable_chars:
+ raise DecodeError("non-printable value")
+ return value
class TeletexString(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,
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")
+ raise DecodeError("invalid UTCTime encoding: %r" % err)
+ 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):
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),
def pps(self, decode_path=()):
yield _pp(
+ obj=self,
asn1_type_name=self.asn1_type_name,
obj_name=self.__class__.__name__,
decode_path=decode_path,
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,
+ ber_encoded=self.ber_encoded,
+ bered=self.bered,
)
for pp in self.pps_lenindef(decode_path):
yield pp
'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:
+ raise DecodeError("invalid GeneralizedTime encoding: %r" % err)
+ 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):
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)
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
def ready(self):
return self._value is not None and self._value[1].ready
+ @property
+ def bered(self):
+ return self.expl_lenindef or (
+ (self._value is not None) and
+ self._value[1].bered
+ )
+
def copy(self):
obj = self.__class__(schema=self.specs)
obj._expl = self._expl
obj.offset = self.offset
obj.llen = self.llen
obj.vlen = self.vlen
+ obj.expl_lenindef = self.expl_lenindef
+ obj.lenindef = self.lenindef
+ obj.ber_encoded = self.ber_encoded
value = self._value
if value is not None:
obj._value = (value[0], value[1].copy())
return self._value[1].encode()
def _decode(self, tlv, offset, decode_path, ctx, tag_only):
- for choice, spec in self.specs.items():
+ for choice, spec in iteritems(self.specs):
sub_decode_path = decode_path + (choice,)
try:
spec.decode(
decode_path=sub_decode_path,
ctx=ctx,
tag_only=True,
+ _ctx_immutable=False,
)
except TagMismatch:
continue
decode_path=decode_path,
offset=offset,
)
- if tag_only:
+ if tag_only: # pragma: no cover
return
value, tail = spec.decode(
tlv,
leavemm=True,
decode_path=sub_decode_path,
ctx=ctx,
+ _ctx_immutable=False,
)
obj = self.__class__(
schema=self.specs,
_decoded=(offset, 0, value.fulllen),
)
obj._value = (choice, value)
- obj.lenindef = value.lenindef
- obj.bered = value.bered
return obj, tail
def __repr__(self):
def pps(self, decode_path=()):
yield _pp(
+ obj=self,
asn1_type_name=self.asn1_type_name,
obj_name=self.__class__.__name__,
decode_path=decode_path,
llen=self.llen,
vlen=self.vlen,
expl_lenindef=self.expl_lenindef,
- lenindef=self.lenindef,
bered=self.bered,
)
if self.ready:
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
def ready(self):
return self._value is not None
+ @property
+ def bered(self):
+ if self.expl_lenindef or self.lenindef:
+ return True
+ if self.defined is None:
+ return False
+ return self.defined[1].bered
+
def copy(self):
obj = self.__class__()
obj._value = self._value
obj.offset = self.offset
obj.llen = self.llen
obj.vlen = self.vlen
+ obj.expl_lenindef = self.expl_lenindef
+ obj.lenindef = self.lenindef
+ obj.ber_encoded = self.ber_encoded
return obj
def __eq__(self, their):
decode_path=decode_path + (str(chunk_i),),
leavemm=True,
ctx=ctx,
+ _ctx_immutable=False,
)
vlen += chunk.tlvlen
sub_offset += chunk.tlvlen
def pps(self, decode_path=()):
yield _pp(
+ obj=self,
asn1_type_name=self.asn1_type_name,
obj_name=self.__class__.__name__,
decode_path=decode_path,
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:
def abs_decode_path(decode_path, rel_path):
"""Create an absolute decode path from current and relative ones
- :param decode_path: current decode path, starting point.
- Tuple of strings
+ :param decode_path: current decode path, starting point. Tuple of strings
:param rel_path: relative path to ``decode_path``. Tuple of strings.
If first tuple's element is "/", then treat it as
an absolute path, ignoring ``decode_path`` as
@property
def ready(self):
- for name, spec in self.specs.items():
+ for name, spec in iteritems(self.specs):
value = self._value.get(name)
if value is None:
if spec.optional:
return False
return True
+ @property
+ def bered(self):
+ if self.expl_lenindef or self.lenindef or self.ber_encoded:
+ return True
+ return any(value.bered for value in itervalues(self._value))
+
def copy(self):
obj = self.__class__(schema=self.specs)
obj.tag = self.tag
obj.offset = self.offset
obj.llen = self.llen
obj.vlen = self.vlen
- obj._value = {k: v.copy() for k, v in self._value.items()}
+ obj.expl_lenindef = self.expl_lenindef
+ obj.lenindef = self.lenindef
+ obj.ber_encoded = self.ber_encoded
+ obj._value = {k: v.copy() for k, v in iteritems(self._value)}
return obj
def __eq__(self, their):
def _encoded_values(self):
raws = []
- for name, spec in self.specs.items():
+ for name, spec in iteritems(self.specs):
value = self._value.get(name)
if value is None:
if spec.optional:
decode_path=decode_path,
offset=offset,
)
- if tag_only:
+ if tag_only: # pragma: no cover
return
lenindef = False
ctx_bered = ctx.get("bered", False)
vlen = 0
sub_offset = offset + tlen + llen
values = {}
- bered = False
+ ber_encoded = False
ctx_allow_default_values = ctx.get("allow_default_values", False)
- for name, spec in self.specs.items():
+ for name, spec in iteritems(self.specs):
if spec.optional and (
(lenindef and v[:EOC_LEN].tobytes() == EOC) or
len(v) == 0
leavemm=True,
decode_path=sub_decode_path,
ctx=ctx,
+ _ctx_immutable=False,
)
except TagMismatch:
if spec.optional:
leavemm=True,
decode_path=sub_sub_decode_path,
ctx=ctx,
+ _ctx_immutable=False,
)
if len(defined_tail) > 0:
raise DecodeError(
leavemm=True,
decode_path=sub_decode_path + (DecodePathDefBy(defined_by),),
ctx=ctx,
+ _ctx_immutable=False,
)
if len(defined_tail) > 0:
raise DecodeError(
v = v_tail
if spec.default is not None and value == spec.default:
if ctx_bered or ctx_allow_default_values:
- bered = True
+ ber_encoded = True
else:
raise DecodeError(
"DEFAULT value met",
)
obj._value = values
obj.lenindef = lenindef
- obj.bered = bered
+ obj.ber_encoded = ber_encoded
return obj, tail
def __repr__(self):
def pps(self, decode_path=()):
yield _pp(
+ obj=self,
asn1_type_name=self.asn1_type_name,
obj_name=self.__class__.__name__,
decode_path=decode_path,
expl_vlen=self.expl_vlen if self.expled else None,
expl_lenindef=self.expl_lenindef,
lenindef=self.lenindef,
+ ber_encoded=self.ber_encoded,
+ bered=self.bered,
)
for name in self.specs:
value = self._value.get(name)
v = b"".join(raws)
return b"".join((self.tag, len_encode(len(v)), v))
+ def _specs_items(self):
+ return iteritems(self.specs)
+
def _decode(self, tlv, offset, decode_path, ctx, tag_only):
try:
t, tlen, lv = tag_strip(tlv)
vlen = 0
sub_offset = offset + tlen + llen
values = {}
- bered = False
+ ber_encoded = False
ctx_allow_default_values = ctx.get("allow_default_values", False)
ctx_allow_unordered_set = ctx.get("allow_unordered_set", False)
value_prev = memoryview(v[:0])
- specs_items = self.specs.items
+
while len(v) > 0:
if lenindef and v[:EOC_LEN].tobytes() == EOC:
break
- for name, spec in specs_items():
+ for name, spec in self._specs_items():
sub_decode_path = decode_path + (name,)
try:
spec.decode(
decode_path=sub_decode_path,
ctx=ctx,
tag_only=True,
+ _ctx_immutable=False,
)
except TagMismatch:
continue
leavemm=True,
decode_path=sub_decode_path,
ctx=ctx,
+ _ctx_immutable=False,
)
value_len = value.fulllen
if value_prev.tobytes() > v[:value_len].tobytes():
if ctx_bered or ctx_allow_unordered_set:
- bered = True
+ ber_encoded = True
else:
raise DecodeError(
"unordered " + self.asn1_type_name,
if spec.default is None or value != spec.default:
pass
elif ctx_bered or ctx_allow_default_values:
- bered = True
+ ber_encoded = True
else:
raise DecodeError(
"DEFAULT value met",
decode_path=decode_path,
offset=offset,
)
- obj.bered = bered
+ obj.ber_encoded = ber_encoded
return obj, tail
def ready(self):
return all(v.ready for v in self._value)
+ @property
+ def bered(self):
+ if self.expl_lenindef or self.lenindef or self.ber_encoded:
+ return True
+ return any(v.bered for v in self._value)
+
def copy(self):
obj = self.__class__(schema=self.spec)
obj._bound_min = self._bound_min
obj.offset = self.offset
obj.llen = self.llen
obj.vlen = self.vlen
+ obj.expl_lenindef = self.expl_lenindef
+ obj.lenindef = self.lenindef
+ obj.ber_encoded = self.ber_encoded
obj._value = [v.copy() for v in self._value]
return obj
_value = []
ctx_allow_unordered_set = ctx.get("allow_unordered_set", False)
value_prev = memoryview(v[:0])
- bered = False
+ ber_encoded = False
spec = self.spec
while len(v) > 0:
if lenindef and v[:EOC_LEN].tobytes() == EOC:
leavemm=True,
decode_path=sub_decode_path,
ctx=ctx,
+ _ctx_immutable=False,
)
value_len = value.fulllen
if ordering_check:
if value_prev.tobytes() > v[:value_len].tobytes():
if ctx_bered or ctx_allow_unordered_set:
- bered = True
+ ber_encoded = True
else:
raise DecodeError(
"unordered " + self.asn1_type_name,
)
obj.lenindef = True
tail = v[EOC_LEN:]
- obj.bered = bered
+ obj.ber_encoded = ber_encoded
return obj, tail
def __repr__(self):
def pps(self, decode_path=()):
yield _pp(
+ obj=self,
asn1_type_name=self.asn1_type_name,
obj_name=self.__class__.__name__,
decode_path=decode_path,
expl_vlen=self.expl_vlen if self.expled else None,
expl_lenindef=self.expl_lenindef,
lenindef=self.lenindef,
+ ber_encoded=self.ber_encoded,
+ bered=self.bered,
)
for i, value in enumerate(self._value):
yield value.pps(decode_path=decode_path + (str(i),))
choice = PrimitiveTypes()
choice.specs["SequenceOf"] = SequenceOf(schema=choice)
choice.specs["SetOf"] = SetOf(schema=choice)
- for i in range(31):
+ for i in six_xrange(31):
choice.specs["SequenceOf%d" % i] = SequenceOf(
schema=choice,
expl=tag_ctxc(i),
def pprint_any(
obj,
- oids=None,
+ oid_maps=(),
with_colours=False,
with_decode_path=False,
decode_path_only=(),
pp = _pp(**pp_kwargs)
yield pp_console_row(
pp,
- oids=oids,
+ oid_maps=oid_maps,
with_offsets=True,
with_blob=False,
with_colours=with_colours,
)
parser.add_argument(
"--oids",
- help="Python path to dictionary with OIDs",
+ help="Python paths to dictionary with OIDs, comma separated",
)
parser.add_argument(
"--schema",
args.DERFile.seek(args.skip)
der = memoryview(args.DERFile.read())
args.DERFile.close()
- oids = obj_by_path(args.oids) if args.oids else {}
+ oid_maps = (
+ [obj_by_path(_path) for _path in (args.oids or "").split(",")]
+ if args.oids else ()
+ )
if args.schema:
schema = obj_by_path(args.schema)
from functools import partial
obj, tail = schema().decode(der, ctx=ctx)
print(pprinter(
obj,
- oids=oids,
+ oid_maps=oid_maps,
with_colours=True if environ.get("NO_COLOR") is None else False,
with_decode_path=args.print_decode_path,
decode_path_only=(