#!/usr/bin/env python
# coding: utf-8
-# PyDERASN -- Python ASN.1 DER codec with abstract structures
+# PyDERASN -- Python ASN.1 DER/BER codec with abstract structures
# Copyright (C) 2017-2018 Sergey Matveev <stargrave@stargrave.org>
#
# This program is free software: you can redistribute it and/or modify
# You should have received a copy of the GNU Lesser General Public
# License along with this program. If not, see
# <http://www.gnu.org/licenses/>.
-"""Python ASN.1 DER codec with abstract structures
+"""Python ASN.1 DER/BER codec with abstract structures
-This library allows you to marshal and unmarshal various structures in
-ASN.1 DER format, like this:
+This library allows you to marshal various structures in ASN.1 DER
+format, unmarshal them in BER/CER/DER ones.
>>> i = Integer(123)
>>> raw = i.encode()
lesser than ``offset``), ``expl_tlen``, ``expl_llen``, ``expl_vlen``
(that actually equals to ordinary ``tlvlen``).
-When error occurs, then :py:exc:`pyderasn.DecodeError` is raised.
+When error occurs, :py:exc:`pyderasn.DecodeError` is raised.
+
+.. _ctx:
+
+Context
+_______
+
+You can specify so called context keyword argument during ``decode()``
+invocation. It is dictionary containing various options governing
+decoding process.
+
+Currently available context options:
+
+* :ref:`bered <bered_ctx>`
+* :ref:`defines_by_path <defines_by_path_ctx>`
+* :ref:`strict_default_existence <strict_default_existence_ctx>`
.. _pprinting:
:py:class:`pyderasn.ObjectIdentifier` field inside
:py:class:`pyderasn.Sequence` can hold mapping between OIDs and
-necessary for decoding structrures. For example, CMS (:rfc:`5652`)
+necessary for decoding structures. For example, CMS (:rfc:`5652`)
container::
class ContentInfo(Sequence):
schema = (
- ("contentType", ContentType(defines=("content", {
+ ("contentType", ContentType(defines=((("content",), {
id_digestedData: DigestedData(),
id_signedData: SignedData(),
- }))),
+ }),))),
("content", Any(expl=tag_ctxc(0))),
)
``contentType`` contains unknown OID, then no automatic decoding is
done.
+You can specify multiple fields, that will be autodecoded -- that is why
+``defines`` kwarg is a sequence. You can specify defined field
+relatively or absolutely to current decode path. For example ``defines``
+for AlgorithmIdentifier of X.509's
+``tbsCertificate.subjectPublicKeyInfo.algorithm.algorithm``::
+
+ (
+ (("parameters",), {
+ id_ecPublicKey: ECParameters(),
+ id_GostR3410_2001: GostR34102001PublicKeyParameters(),
+ }),
+ (("..", "subjectPublicKey"), {
+ id_rsaEncryption: RSAPublicKey(),
+ id_GostR3410_2001: OctetString(),
+ }),
+ ),
+
+tells that if certificate's SPKI algorithm is GOST R 34.10-2001, then
+autodecode its parameters inside SPKI's algorithm and its public key
+itself.
+
Following types can be automatically decoded (DEFINED BY):
* :py:class:`pyderasn.Any`
* :py:class:`pyderasn.BitString` (that is multiple of 8 bits)
* :py:class:`pyderasn.OctetString`
* :py:class:`pyderasn.SequenceOf`/:py:class:`pyderasn.SetOf`
- ``Any``/``OctetString``-s
+ ``Any``/``BitString``/``OctetString``-s
When any of those fields is automatically decoded, then ``.defined``
attribute contains ``(OID, value)`` tuple. ``OID`` tells by which OID it
above, ``content_info["content"].defined == (id_signedData,
signed_data)``.
-.. _defines_by_path_kwarg:
+.. _defines_by_path_ctx:
-defines_by_path kwarg
-_____________________
+defines_by_path context option
+______________________________
Sometimes you either can not or do not want to explicitly set *defines*
in the scheme. You can dynamically apply those definitions when calling
``.decode()`` method.
-Decode method takes optional ``defines_by_path`` keyword argument that
-must be sequence of following tuples::
+Specify ``defines_by_path`` key in the :ref:`decode context <ctx>`. Its
+value must be sequence of following tuples::
(decode_path, defines)
content_info, tail = ContentInfo().decode(data, defines_by_path=(
(
("contentType",),
- ("content", {id_signedData: SignedData()}),
+ ((("content",), {id_signedData: SignedData()}),),
),
(
(
"content",
- decode_path_defby(id_signedData),
+ DecodePathDefBy(id_signedData),
"encapContentInfo",
"eContentType",
),
- ("eContent", {
+ ((("eContent",), {
id_cct_PKIData: PKIData(),
id_cct_PKIResponse: PKIResponse(),
- }),
+ })),
),
(
(
"content",
- decode_path_defby(id_signedData),
+ DecodePathDefBy(id_signedData),
"encapContentInfo",
"eContent",
- decode_path_defby(id_cct_PKIResponse),
+ DecodePathDefBy(id_cct_PKIResponse),
"controlSequence",
any,
"attrType",
),
- ("attrValues", {
+ ((("attrValues",), {
id_cmc_recipientNonce: RecipientNonce(),
id_cmc_senderNonce: SenderNonce(),
id_cmc_statusInfoV2: CMCStatusInfoV2(),
id_cmc_transactionId: TransactionId(),
- }),
+ })),
),
))
-Pay attention for :py:func:`pyderasn.decode_path_defby` and ``any``.
+Pay attention for :py:class:`pyderasn.DecodePathDefBy` and ``any``.
First function is useful for path construction when some automatic
decoding is already done. ``any`` means literally any value it meet --
useful for SEQUENCE/SET OF-s.
+.. _bered_ctx:
+
+BER encoding
+------------
+
+.. warning::
+
+ Currently BER support is not extensively tested.
+
+By default PyDERASN accepts only DER encoded data. It always encodes to
+DER. But you can optionally enable BER decoding with setting ``bered``
+:ref:`context <ctx>` argument to True. Indefinite lengths and
+constructed primitive types should be parsed successfully.
+
+* If object is encoded in BER form (not the DER one), then ``bered``
+ attribute is set to True. Only ``BOOLEAN``, ``BIT STRING``, ``OCTET
+ STRING`` can contain it.
+* If object has an indefinite length encoding, then its ``lenindef``
+ attribute is set to True. Only ``BIT STRING``, ``OCTET STRING``,
+ ``SEQUENCE``, ``SET``, ``SEQUENCE OF``, ``SET OF``, ``ANY`` can
+ contain it.
+* If object has an indefinite length encoded explicit tag, then
+ ``expl_lenindef`` is set to True.
+
+EOC (end-of-contents) token's length is taken in advance in object's
+value length.
+
Primitive types
---------------
Various
-------
+.. autofunction:: pyderasn.abs_decode_path
.. autofunction:: pyderasn.hexenc
.. autofunction:: pyderasn.hexdec
.. autofunction:: pyderasn.tag_encode
from collections import OrderedDict
from datetime import datetime
from math import ceil
+from os import environ
+from string import digits
from six import add_metaclass
from six import binary_type
from six.moves import xrange as six_xrange
+try:
+ from termcolor import colored
+except ImportError:
+ def colored(what, *args):
+ return what
+
+
__all__ = (
"Any",
"BitString",
"Boolean",
"BoundsError",
"Choice",
- "decode_path_defby",
"DecodeError",
+ "DecodePathDefBy",
"Enumerated",
"GeneralizedTime",
"GeneralString",
TagClassPrivate: "PRIVATE ",
TagClassUniversal: "UNIV ",
}
+EOC = b"\x00\x00"
+EOC_LEN = len(EOC)
########################################################################
c for c in (
"" if self.klass is None else self.klass.__name__,
(
- ("(%s)" % ".".join(self.decode_path))
+ ("(%s)" % ".".join(str(dp) for dp in self.decode_path))
if len(self.decode_path) > 0 else ""
),
("(at %d)" % self.offset) if self.offset > 0 else "",
pass
+class LenIndefForm(DecodeError):
+ pass
+
+
class TagMismatch(DecodeError):
pass
def len_decode(data):
+ """Decode length
+
+ :returns: (decoded length, length's length, remaining data)
+ :raises LenIndefForm: if indefinite form encoding is met
+ """
if len(data) == 0:
raise NotEnoughData("no data at all")
first_octet = byte2int(data)
if octets_num + 1 > len(data):
raise NotEnoughData("encoded length is longer than data")
if octets_num == 0:
- raise DecodeError("long form instead of short one")
+ raise LenIndefForm()
if byte2int(data[1:]) == 0:
raise DecodeError("leading zeros")
l = 0
"offset",
"llen",
"vlen",
+ "expl_lenindef",
+ "lenindef",
+ "bered",
)
def __init__(
self.optional = optional
self.offset, self.llen, self.vlen = _decoded
self.default = None
+ self.expl_lenindef = False
+ self.lenindef = False
+ self.bered = False
@property
def ready(self): # pragma: no cover
def _encode(self): # pragma: no cover
raise NotImplementedError()
- def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None): # pragma: no cover
+ def _decode(self, tlv, offset, decode_path, ctx, tag_only): # pragma: no cover
raise NotImplementedError()
def encode(self):
return raw
return b"".join((self._expl, len_encode(len(raw)), raw))
- def decode(self, data, offset=0, leavemm=False, decode_path=(), defines_by_path=None):
+ def decode(
+ self,
+ data,
+ offset=0,
+ leavemm=False,
+ decode_path=(),
+ ctx=None,
+ tag_only=False,
+ ):
"""Decode the data
:param data: either binary or memoryview
:param int offset: initial data's offset
:param bool leavemm: do we need to leave memoryview of remaining
data as is, or convert it to bytes otherwise
- :param defines_by_path: :ref:`Read about DEFINED BY <definedby>`
+ :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)
:returns: (Obj, remaining data)
"""
+ if ctx is None:
+ ctx = {}
tlv = memoryview(data)
if self._expl is None:
- obj, tail = self._decode(
+ result = self._decode(
tlv,
offset,
decode_path=decode_path,
- defines_by_path=defines_by_path,
+ ctx=ctx,
+ tag_only=tag_only,
)
+ if tag_only:
+ return
+ obj, tail = result
else:
try:
t, tlen, lv = tag_strip(tlv)
)
try:
l, llen, v = len_decode(lv)
+ except LenIndefForm as err:
+ if not ctx.get("bered", False):
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ llen, v = 1, lv[1:]
+ offset += tlen + llen
+ result = self._decode(
+ v,
+ offset=offset,
+ decode_path=decode_path,
+ ctx=ctx,
+ tag_only=tag_only,
+ )
+ if tag_only:
+ return
+ obj, tail = result
+ eoc_expected, tail = tail[:EOC_LEN], tail[EOC_LEN:]
+ if eoc_expected.tobytes() != EOC:
+ raise DecodeError(
+ msg="no EOC",
+ decode_path=decode_path,
+ offset=offset,
+ )
+ obj.vlen += EOC_LEN
+ obj.expl_lenindef = True
except DecodeError as err:
raise err.__class__(
msg=err.msg,
decode_path=decode_path,
offset=offset,
)
- if l > len(v):
- raise NotEnoughData(
- "encoded length is longer than data",
- klass=self.__class__,
+ else:
+ if l > len(v):
+ raise NotEnoughData(
+ "encoded length is longer than data",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ result = self._decode(
+ v,
+ offset=offset + tlen + llen,
decode_path=decode_path,
- offset=offset,
+ ctx=ctx,
+ tag_only=tag_only,
)
- obj, tail = self._decode(
- v,
- offset=offset + tlen + llen,
- decode_path=decode_path,
- defines_by_path=defines_by_path,
- )
+ if tag_only:
+ return
+ obj, tail = result
return obj, (tail if leavemm else tail.tobytes())
@property
@property
def expl_llen(self):
+ if self.expl_lenindef:
+ return 1
return len(len_encode(self.tlvlen))
@property
return self.expl_tlen + self.expl_llen + self.expl_vlen
-def decode_path_defby(defined_by):
+class DecodePathDefBy(object):
"""DEFINED BY representation inside decode path
"""
- return "DEFINED BY (%s)" % defined_by
+ __slots__ = ("defined_by",)
+
+ def __init__(self, defined_by):
+ self.defined_by = defined_by
+
+ def __ne__(self, their):
+ return not(self == their)
+
+ def __eq__(self, their):
+ if not isinstance(their, self.__class__):
+ return False
+ return self.defined_by == their.defined_by
+
+ def __str__(self):
+ return "DEFINED BY " + str(self.defined_by)
+
+ def __repr__(self):
+ return "<%s: %s>" % (self.__class__.__name__, self.defined_by)
########################################################################
"expl_tlen",
"expl_llen",
"expl_vlen",
+ "expl_lenindef",
+ "lenindef",
+ "bered",
))
expl_tlen=None,
expl_llen=None,
expl_vlen=None,
+ expl_lenindef=False,
+ lenindef=False,
+ bered=False,
):
return PP(
asn1_type_name,
expl_tlen,
expl_llen,
expl_vlen,
+ expl_lenindef,
+ lenindef,
+ bered,
)
-def pp_console_row(pp, oids=None, with_offsets=False, with_blob=True):
+def _colorize(what, colour, with_colours, attrs=("bold",)):
+ return colored(what, colour, attrs=attrs) if with_colours else what
+
+
+def pp_console_row(
+ pp,
+ oids=None,
+ with_offsets=False,
+ with_blob=True,
+ with_colours=False,
+):
cols = []
if with_offsets:
- cols.append("%5d%s [%d,%d,%4d]" % (
+ col = "%5d%s" % (
pp.offset,
(
" " if pp.expl_offset is None else
("-%d" % (pp.offset - pp.expl_offset))
),
- pp.tlen,
- pp.llen,
- pp.vlen,
- ))
+ )
+ cols.append(_colorize(col, "red", with_colours, ()))
+ col = "[%d,%d,%4d]" % (pp.tlen, pp.llen, pp.vlen)
+ col = _colorize(col, "green", with_colours, ())
+ ber_deoffset = 0
+ if pp.expl_lenindef:
+ ber_deoffset += 2
+ if pp.lenindef:
+ ber_deoffset += 2
+ col += (
+ " " if ber_deoffset == 0 else
+ _colorize(("-%d" % ber_deoffset), "red", with_colours)
+ )
+ cols.append(col)
if len(pp.decode_path) > 0:
cols.append(" ." * (len(pp.decode_path)))
- cols.append("%s:" % pp.decode_path[-1])
+ ent = pp.decode_path[-1]
+ if isinstance(ent, DecodePathDefBy):
+ cols.append(_colorize("DEFINED BY", "red", with_colours, ("reverse",)))
+ value = str(ent.defined_by)
+ if (
+ oids is not None and
+ ent.defined_by.asn1_type_name ==
+ ObjectIdentifier.asn1_type_name and
+ value in oids
+ ):
+ cols.append(_colorize("%s:" % oids[value], "green", with_colours))
+ else:
+ cols.append(_colorize("%s:" % value, "white", with_colours, ("reverse",)))
+ else:
+ cols.append(_colorize("%s:" % ent, "yellow", with_colours, ("reverse",)))
if pp.expl is not None:
klass, _, num = pp.expl
- cols.append("[%s%d] EXPLICIT" % (TagClassReprs[klass], num))
+ col = "[%s%d] EXPLICIT" % (TagClassReprs[klass], num)
+ cols.append(_colorize(col, "blue", with_colours))
if pp.impl is not None:
klass, _, num = pp.impl
- cols.append("[%s%d]" % (TagClassReprs[klass], num))
+ col = "[%s%d]" % (TagClassReprs[klass], num)
+ cols.append(_colorize(col, "blue", with_colours))
if pp.asn1_type_name.replace(" ", "") != pp.obj_name.upper():
- cols.append(pp.obj_name)
- cols.append(pp.asn1_type_name)
+ cols.append(_colorize(pp.obj_name, "magenta", with_colours))
+ if pp.bered:
+ cols.append(_colorize("BER", "red", with_colours))
+ cols.append(_colorize(pp.asn1_type_name, "cyan", with_colours))
if pp.value is not None:
value = pp.value
+ cols.append(_colorize(value, "white", with_colours, ("reverse",)))
if (
oids is not None and
pp.asn1_type_name == ObjectIdentifier.asn1_type_name and
value in oids
):
- value = "%s (%s)" % (oids[value], pp.value)
- cols.append(value)
+ cols.append(_colorize("(%s)" % oids[value], "green", with_colours))
if with_blob:
if isinstance(pp.blob, binary_type):
cols.append(hexenc(pp.blob))
elif isinstance(pp.blob, tuple):
cols.append(", ".join(pp.blob))
if pp.optional:
- cols.append("OPTIONAL")
+ cols.append(_colorize("OPTIONAL", "red", with_colours))
if pp.default:
- cols.append("DEFAULT")
+ cols.append(_colorize("DEFAULT", "red", with_colours))
return " ".join(cols)
def pp_console_blob(pp):
- cols = [" " * len("XXXXXYY [X,X,XXXX]")]
+ cols = [" " * len("XXXXXYY [X,X,XXXX]YY")]
if len(pp.decode_path) > 0:
cols.append(" ." * (len(pp.decode_path) + 1))
if isinstance(pp.blob, binary_type):
yield " ".join(cols + [", ".join(pp.blob)])
-def pprint(obj, oids=None, big_blobs=False):
+def pprint(obj, oids=None, big_blobs=False, with_colours=False):
"""Pretty print object
:param Obj obj: object you want to pretty print
:param big_blobs: if large binary objects are met (like OctetString
values), do we need to print them too, on separate
lines
+ :param with_colours: colourize output, if ``termcolor`` library
+ is available
"""
def _pprint_pps(pps):
for pp in pps:
oids=oids,
with_offsets=True,
with_blob=False,
+ with_colours=with_colours,
)
for row in pp_console_blob(pp):
yield row
else:
- yield pp_console_row(pp, oids=oids, with_offsets=True)
+ yield pp_console_row(
+ pp,
+ oids=oids,
+ with_offsets=True,
+ with_blob=True,
+ with_colours=with_colours,
+ )
else:
for row in _pprint_pps(pp):
yield row
(b"\xFF" if self._value else b"\x00"),
))
- def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
+ def _decode(self, tlv, offset, decode_path, ctx, tag_only):
try:
t, _, lv = tag_strip(tlv)
except DecodeError as err:
decode_path=decode_path,
offset=offset,
)
+ if tag_only:
+ return
try:
l, _, v = len_decode(lv)
except DecodeError as err:
offset=offset,
)
first_octet = byte2int(v)
+ bered = False
if first_octet == 0:
value = False
elif first_octet == 0xFF:
value = True
+ elif ctx.get("bered", False):
+ value = True
+ bered = True
else:
raise DecodeError(
"unacceptable Boolean value",
optional=self.optional,
_decoded=(offset, 1, 1),
)
+ obj.bered = bered
return obj, v[1:]
def __repr__(self):
expl_tlen=self.expl_tlen if self.expled else None,
expl_llen=self.expl_llen if self.expled else None,
expl_vlen=self.expl_vlen if self.expled else None,
+ expl_lenindef=self.expl_lenindef,
+ bered=self.bered,
)
break
return b"".join((self.tag, len_encode(len(octets)), octets))
- def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
+ def _decode(self, tlv, offset, decode_path, ctx, tag_only):
try:
t, _, lv = tag_strip(tlv)
except DecodeError as err:
decode_path=decode_path,
offset=offset,
)
+ if tag_only:
+ return
try:
l, llen, v = len_decode(lv)
except DecodeError as err:
expl_tlen=self.expl_tlen if self.expled else None,
expl_llen=self.expl_llen if self.expled else None,
expl_vlen=self.expl_vlen if self.expled else None,
+ expl_lenindef=self.expl_lenindef,
)
>>> b.bit_len
88
+ >>> BitString("'0A3B5F291CD'H")
+ BIT STRING 44 bits 0a3b5f291cd0
>>> b = BitString("'010110000000'B")
BIT STRING 12 bits 5800
>>> b.bit_len
class KeyUsage(BitString):
schema = (
- ('digitalSignature', 0),
- ('nonRepudiation', 1),
- ('keyEncipherment', 2),
+ ("digitalSignature", 0),
+ ("nonRepudiation", 1),
+ ("keyEncipherment", 2),
)
- >>> b = KeyUsage(('keyEncipherment', 'nonRepudiation'))
+ >>> b = KeyUsage(("keyEncipherment", "nonRepudiation"))
KeyUsage BIT STRING 3 bits nonRepudiation, keyEncipherment
>>> b.named
['nonRepudiation', 'keyEncipherment']
>>> b.specs
{'nonRepudiation': 1, 'digitalSignature': 0, 'keyEncipherment': 2}
"""
- __slots__ = ("specs", "defined")
+ __slots__ = ("tag_constructed", "specs", "defined")
tag_default = tag_encode(3)
asn1_type_name = "BIT STRING"
if value is None:
self._value = default
self.defined = None
+ tag_klass, _, tag_num = tag_decode(self.tag)
+ self.tag_constructed = tag_encode(
+ klass=tag_klass,
+ form=TagFormConstructed,
+ num=tag_num,
+ )
def _bits2octets(self, bits):
if len(self.specs) > 0:
if isinstance(value, (string_types, binary_type)):
if (
isinstance(value, string_types) and
- value.startswith("'") and
- value.endswith("'B")
+ value.startswith("'")
):
- value = value[1:-2]
- if not set(value) <= set(("0", "1")):
- raise ValueError("B's coding contains unacceptable chars")
- return self._bits2octets(value)
+ if value.endswith("'B"):
+ value = value[1:-2]
+ if not set(value) <= set(("0", "1")):
+ raise ValueError("B's coding contains unacceptable chars")
+ return self._bits2octets(value)
+ elif value.endswith("'H"):
+ value = value[1:-2]
+ return (
+ len(value) * 4,
+ hexdec(value + ("" if len(value) % 2 == 0 else "0")),
+ )
+ else:
+ raise InvalidValueType((self.__class__, string_types, binary_type))
elif isinstance(value, binary_type):
return (len(value) * 8, value)
else:
- raise InvalidValueType((
- self.__class__,
- string_types,
- binary_type,
- ))
+ raise InvalidValueType((self.__class__, string_types, binary_type))
if isinstance(value, tuple):
if (
len(value) == 2 and
octets,
))
- def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
- try:
- t, _, lv = tag_strip(tlv)
- except DecodeError as err:
- raise err.__class__(
- msg=err.msg,
- klass=self.__class__,
- decode_path=decode_path,
- offset=offset,
- )
- if t != self.tag:
- raise TagMismatch(
- klass=self.__class__,
- decode_path=decode_path,
- offset=offset,
- )
+ def _decode_chunk(self, lv, offset, decode_path, ctx):
try:
l, llen, v = len_decode(lv)
except DecodeError as err:
decode_path=decode_path,
offset=offset,
)
- if byte2int(v[-1:]) & ((1 << pad_size) - 1) != 0:
+ if byte2int(v[l - 1:l]) & ((1 << pad_size) - 1) != 0:
raise DecodeError(
"invalid pad",
klass=self.__class__,
)
return obj, tail
+ def _decode(self, tlv, offset, decode_path, ctx, tag_only):
+ try:
+ t, tlen, lv = tag_strip(tlv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if t == self.tag:
+ if tag_only:
+ return
+ return self._decode_chunk(lv, offset, decode_path, ctx)
+ if t == self.tag_constructed:
+ if not ctx.get("bered", False):
+ raise DecodeError(
+ msg="unallowed BER constructed encoding",
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if tag_only:
+ return
+ 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 > 0 and 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(
+ msg="chunk out of bounds",
+ decode_path=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,
+ )
+ except TagMismatch:
+ raise DecodeError(
+ msg="expected BitString encoded chunk",
+ 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(
+ msg="no chunks",
+ 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(
+ msg="BitString chunk is not multiple of 8 bit",
+ 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.bered = True
+ return obj, (v[EOC_LEN:] if lenindef else v)
+ raise TagMismatch(
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+
def __repr__(self):
return pp_console_row(next(self.pps()))
expl_tlen=self.expl_tlen if self.expled else None,
expl_llen=self.expl_llen if self.expled else None,
expl_vlen=self.expl_vlen if self.expled else None,
+ expl_lenindef=self.expl_lenindef,
+ lenindef=self.lenindef,
+ bered=self.bered,
)
defined_by, defined = self.defined or (None, None)
if defined_by is not None:
yield defined.pps(
- decode_path=decode_path + (decode_path_defby(defined_by),)
+ decode_path=decode_path + (DecodePathDefBy(defined_by),)
)
>>> OctetString(b"hell", bounds=(4, 4))
OCTET STRING 4 bytes 68656c6c
"""
- __slots__ = ("_bound_min", "_bound_max", "defined")
+ __slots__ = ("tag_constructed", "_bound_min", "_bound_max", "defined")
tag_default = tag_encode(4)
asn1_type_name = "OCTET STRING"
if self._value is None:
self._value = default
self.defined = None
+ tag_klass, _, tag_num = tag_decode(self.tag)
+ self.tag_constructed = tag_encode(
+ klass=tag_klass,
+ form=TagFormConstructed,
+ num=tag_num,
+ )
def _value_sanitize(self, value):
if issubclass(value.__class__, OctetString):
self._value,
))
- def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
- try:
- t, _, lv = tag_strip(tlv)
- except DecodeError as err:
- raise err.__class__(
- msg=err.msg,
- klass=self.__class__,
- decode_path=decode_path,
- offset=offset,
- )
- if t != self.tag:
- raise TagMismatch(
- klass=self.__class__,
- decode_path=decode_path,
- offset=offset,
- )
+ def _decode_chunk(self, lv, offset, decode_path, ctx):
try:
l, llen, v = len_decode(lv)
except DecodeError as err:
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),
)
return obj, tail
+ def _decode(self, tlv, offset, decode_path, ctx, tag_only):
+ try:
+ t, tlen, lv = tag_strip(tlv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if t == self.tag:
+ if tag_only:
+ return
+ return self._decode_chunk(lv, offset, decode_path, ctx)
+ if t == self.tag_constructed:
+ if not ctx.get("bered", False):
+ raise DecodeError(
+ msg="unallowed BER constructed encoding",
+ decode_path=decode_path,
+ offset=offset,
+ )
+ if tag_only:
+ return
+ 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 > 0 and 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(
+ msg="chunk out of bounds",
+ decode_path=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,
+ )
+ except TagMismatch:
+ raise DecodeError(
+ msg="expected OctetString encoded chunk",
+ 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(
+ msg="no chunks",
+ decode_path=decode_path,
+ offset=offset,
+ )
+ 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)),
+ )
+ 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.bered = True
+ return obj, (v[EOC_LEN:] if lenindef else v)
+ raise TagMismatch(
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+
def __repr__(self):
return pp_console_row(next(self.pps()))
expl_tlen=self.expl_tlen if self.expled else None,
expl_llen=self.expl_llen if self.expled else None,
expl_vlen=self.expl_vlen if self.expled else None,
+ expl_lenindef=self.expl_lenindef,
+ lenindef=self.lenindef,
+ bered=self.bered,
)
defined_by, defined = self.defined or (None, None)
if defined_by is not None:
yield defined.pps(
- decode_path=decode_path + (decode_path_defby(defined_by),)
+ decode_path=decode_path + (DecodePathDefBy(defined_by),)
)
def _encode(self):
return self.tag + len_encode(0)
- def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
+ def _decode(self, tlv, offset, decode_path, ctx, tag_only):
try:
t, _, lv = tag_strip(tlv)
except DecodeError as err:
decode_path=decode_path,
offset=offset,
)
+ if tag_only:
+ return
try:
l, _, v = len_decode(lv)
except DecodeError as err:
expl_tlen=self.expl_tlen if self.expled else None,
expl_llen=self.expl_llen if self.expled else None,
expl_vlen=self.expl_vlen if self.expled else None,
+ expl_lenindef=self.expl_lenindef,
)
def __init__(
self,
value=None,
- defines=None,
+ defines=(),
impl=None,
expl=None,
default=None,
:param value: set the value. Either tuples of integers,
string of "."-concatenated integers, or
:py:class:`pyderasn.ObjectIdentifier` object
- :param defines: tuple of two elements. First one is a name of
- field inside :py:class:`pyderasn.Sequence`,
- defining with that OID. Second element is a
- ``{OID: pyderasn.Obj()}`` dictionary, mapping
- between current OID value and structure applied
- to defined field.
+ :param defines: sequence of tuples. Each tuple has two elements.
+ First one is relative to current one decode
+ path, aiming to the field defined by that OID.
+ Read about relative path in
+ :py:func:`pyderasn.abs_decode_path`. Second
+ tuple element is ``{OID: pyderasn.Obj()}``
+ dictionary, mapping between current OID value
+ and structure applied to defined field.
:ref:`Read about DEFINED BY <definedby>`
:param bytes impl: override default tag with ``IMPLICIT`` one
:param bytes expl: override default tag with ``EXPLICIT`` one
v = b"".join(octets)
return b"".join((self.tag, len_encode(len(v)), v))
- def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
+ def _decode(self, tlv, offset, decode_path, ctx, tag_only):
try:
t, _, lv = tag_strip(tlv)
except DecodeError as err:
decode_path=decode_path,
offset=offset,
)
+ if tag_only:
+ return
try:
l, llen, v = len_decode(lv)
except DecodeError as err:
expl_tlen=self.expl_tlen if self.expled else None,
expl_llen=self.expl_llen if self.expled else None,
expl_vlen=self.expl_vlen if self.expled else None,
+ expl_lenindef=self.expl_lenindef,
)
>>> PrintableString("привет мир")
Traceback (most recent call last):
- UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-5: ordinal not in range(128)
+ pyderasn.DecodeError: 'ascii' codec can't encode characters in position 0-5: ordinal not in range(128)
>>> BMPString("ада", bounds=(2, 2))
Traceback (most recent call last):
value_raw = value
else:
raise InvalidValueType((self.__class__, text_type, binary_type))
- value_raw = (
- value_decoded.encode(self.encoding)
- if value_raw is None else value_raw
- )
- value_decoded = (
- value_raw.decode(self.encoding)
- if value_decoded is None else value_decoded
- )
+ try:
+ value_raw = (
+ value_decoded.encode(self.encoding)
+ if value_raw is None else value_raw
+ )
+ value_decoded = (
+ value_raw.decode(self.encoding)
+ if value_decoded is None else value_decoded
+ )
+ except (UnicodeEncodeError, UnicodeDecodeError) as err:
+ raise DecodeError(str(err))
if not self._bound_min <= len(value_decoded) <= self._bound_max:
raise BoundsError(
self._bound_min,
tlen=self.tlen,
llen=self.llen,
vlen=self.vlen,
+ expl_offset=self.expl_offset if self.expled else None,
+ expl_tlen=self.expl_tlen if self.expled else None,
+ expl_llen=self.expl_llen if self.expled else None,
+ expl_vlen=self.expl_vlen if self.expled else None,
+ expl_lenindef=self.expl_lenindef,
)
tag_default = tag_encode(18)
encoding = "ascii"
asn1_type_name = "NumericString"
+ allowable_chars = set(digits.encode("ascii"))
+
+ def _value_sanitize(self, value):
+ value = super(NumericString, self)._value_sanitize(value)
+ if not set(value) <= self.allowable_chars:
+ raise DecodeError("non-numeric value")
+ return value
class PrintableString(CommonString):
tlen=self.tlen,
llen=self.llen,
vlen=self.vlen,
+ expl_offset=self.expl_offset if self.expled else None,
+ expl_tlen=self.expl_tlen if self.expled else None,
+ expl_llen=self.expl_llen if self.expled else None,
+ expl_vlen=self.expl_vlen if self.expled else None,
+ expl_lenindef=self.expl_lenindef,
)
class GeneralName(Choice):
schema = (
- ('rfc822Name', IA5String(impl=tag_ctxp(1))),
- ('dNSName', IA5String(impl=tag_ctxp(2))),
+ ("rfc822Name", IA5String(impl=tag_ctxp(1))),
+ ("dNSName", IA5String(impl=tag_ctxp(2))),
)
>>> gn = GeneralName()
self._assert_ready()
return self._value[1].encode()
- def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
+ def _decode(self, tlv, offset, decode_path, ctx, tag_only):
for choice, spec in self.specs.items():
+ sub_decode_path = decode_path + (choice,)
try:
- value, tail = spec.decode(
+ spec.decode(
tlv,
offset=offset,
leavemm=True,
- decode_path=decode_path + (choice,),
- defines_by_path=defines_by_path,
+ decode_path=sub_decode_path,
+ ctx=ctx,
+ tag_only=True,
)
except TagMismatch:
continue
- obj = self.__class__(
- schema=self.specs,
- expl=self._expl,
- default=self.default,
- optional=self.optional,
- _decoded=(offset, 0, value.tlvlen),
+ break
+ else:
+ raise TagMismatch(
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
)
- obj._value = (choice, value)
- return obj, tail
- raise TagMismatch(
- klass=self.__class__,
- decode_path=decode_path,
+ if tag_only:
+ return
+ value, tail = spec.decode(
+ tlv,
offset=offset,
+ leavemm=True,
+ decode_path=sub_decode_path,
+ ctx=ctx,
+ )
+ obj = self.__class__(
+ schema=self.specs,
+ expl=self._expl,
+ default=self.default,
+ optional=self.optional,
+ _decoded=(offset, 0, value.tlvlen),
)
+ obj._value = (choice, value)
+ return obj, tail
def __repr__(self):
value = pp_console_row(next(self.pps()))
tlen=self.tlen,
llen=self.llen,
vlen=self.vlen,
+ expl_lenindef=self.expl_lenindef,
)
if self.ready:
yield self.value.pps(decode_path=decode_path + (self.choice,))
self._assert_ready()
return self._value
- def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
+ def _decode(self, tlv, offset, decode_path, ctx, tag_only):
try:
t, tlen, lv = tag_strip(tlv)
+ except DecodeError as err:
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ try:
l, llen, v = len_decode(lv)
+ except LenIndefForm as err:
+ if not ctx.get("bered", False):
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ llen, vlen, v = 1, 0, lv[1:]
+ sub_offset = offset + tlen + llen
+ chunk_i = 0
+ while True:
+ if v[:EOC_LEN].tobytes() == EOC:
+ tlvlen = tlen + llen + vlen + EOC_LEN
+ obj = self.__class__(
+ value=tlv[:tlvlen].tobytes(),
+ expl=self._expl,
+ optional=self.optional,
+ _decoded=(offset, 0, tlvlen),
+ )
+ obj.lenindef = True
+ obj.tag = t
+ return obj, v[EOC_LEN:]
+ else:
+ chunk, v = Any().decode(
+ v,
+ offset=sub_offset,
+ decode_path=decode_path + (str(chunk_i),),
+ leavemm=True,
+ ctx=ctx,
+ )
+ vlen += chunk.tlvlen
+ sub_offset += chunk.tlvlen
+ chunk_i += 1
except DecodeError as err:
raise err.__class__(
msg=err.msg,
expl_tlen=self.expl_tlen if self.expled else None,
expl_llen=self.expl_llen if self.expled else None,
expl_vlen=self.expl_vlen if self.expled else None,
+ expl_lenindef=self.expl_lenindef,
+ lenindef=self.lenindef,
)
defined_by, defined = self.defined or (None, None)
if defined_by is not None:
yield defined.pps(
- decode_path=decode_path + (decode_path_defby(defined_by),)
+ decode_path=decode_path + (DecodePathDefBy(defined_by),)
)
return define
+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 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
+ starting point. Also this tuple can contain ".."
+ elements, stripping the leading element from
+ ``decode_path``
+
+ >>> abs_decode_path(("foo", "bar"), ("baz", "whatever"))
+ ("foo", "bar", "baz", "whatever")
+ >>> abs_decode_path(("foo", "bar", "baz"), ("..", "..", "whatever"))
+ ("foo", "whatever")
+ >>> abs_decode_path(("foo", "bar"), ("/", "baz", "whatever"))
+ ("baz", "whatever")
+ """
+ if rel_path[0] == "/":
+ return rel_path[1:]
+ if rel_path[0] == "..":
+ return abs_decode_path(decode_path[:-1], rel_path[1:])
+ return decode_path + rel_path
+
+
class Sequence(Obj):
"""``SEQUENCE`` structure type
pyderasn.InvalidValueType: invalid value type, expected: <class 'pyderasn.ObjectIdentifier'>
>>> ext["extnID"] = ObjectIdentifier("1.2.3")
- You can know if sequence is ready to be encoded:
+ You can determine if sequence is ready to be encoded:
>>> ext.ready
False
>>> tbs = TBSCertificate()
>>> tbs["version"] = Version("v2") # no need to explicitly add ``expl``
- You can know if value exists/set in the sequence and take its value:
+ Assign ``None`` to remove value from sequence.
+
+ You can set values in Sequence during its initialization:
+
+ >>> AlgorithmIdentifier((
+ ("algorithm", ObjectIdentifier("1.2.3")),
+ ("parameters", Any(Null()))
+ ))
+ AlgorithmIdentifier SEQUENCE[OBJECT IDENTIFIER 1.2.3, ANY 0500 OPTIONAL]
+
+ You can determine if value exists/set in the sequence and take its value:
>>> "extnID" in ext, "extnValue" in ext, "critical" in ext
(True, True, False)
All defaulted values are always optional.
+ .. _strict_default_existence_ctx:
+
.. warning::
When decoded DER contains defaulted value inside, then
- technically this is not valid DER encoding. But we allow
- and pass it. Of course reencoding of that kind of DER will
+ technically this is not valid DER encoding. But we allow and pass
+ it **by default**. Of course reencoding of that kind of DER will
result in different binary representation (validly without
- defaulted value inside).
+ defaulted value inside). You can enable strict defaulted values
+ existence validation by setting ``"strict_default_existence":
+ True`` :ref:`context <ctx>` option -- decoding process will raise
+ an exception if defaulted value is met.
Two sequences are equal if they have equal specification (schema),
implicit/explicit tagging and the same values.
)
self._value = {}
if value is not None:
- self._value = self._value_sanitize(value)
+ if issubclass(value.__class__, Sequence):
+ self._value = value._value
+ elif hasattr(value, "__iter__"):
+ for seq_key, seq_value in value:
+ self[seq_key] = seq_value
+ else:
+ raise InvalidValueType((Sequence,))
if default is not None:
- default_value = self._value_sanitize(default)
+ if not issubclass(default.__class__, Sequence):
+ raise InvalidValueType((Sequence,))
+ default_value = default._value
default_obj = self.__class__(impl=self.tag, expl=self._expl)
default_obj.specs = self.specs
default_obj._value = default_value
if value is None:
self._value = default_obj.copy()._value
- def _value_sanitize(self, value):
- if not issubclass(value.__class__, Sequence):
- raise InvalidValueType((Sequence,))
- return value._value
-
@property
def ready(self):
for name, spec in self.specs.items():
v = b"".join(self._encoded_values())
return b"".join((self.tag, len_encode(len(v)), v))
- def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
+ def _decode(self, tlv, offset, decode_path, ctx, tag_only):
try:
t, tlen, lv = tag_strip(tlv)
except DecodeError as err:
decode_path=decode_path,
offset=offset,
)
+ if tag_only:
+ return
+ lenindef = False
try:
l, llen, v = len_decode(lv)
+ except LenIndefForm as err:
+ if not ctx.get("bered", False):
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ l, llen, v = 0, 1, lv[1:]
+ lenindef = True
except DecodeError as err:
raise err.__class__(
msg=err.msg,
decode_path=decode_path,
offset=offset,
)
- v, tail = v[:l], v[l:]
+ if not lenindef:
+ v, tail = v[:l], v[l:]
+ vlen = 0
sub_offset = offset + tlen + llen
values = {}
- defines = {}
for name, spec in self.specs.items():
- if len(v) == 0 and spec.optional:
+ if spec.optional and (
+ (lenindef and v[:EOC_LEN].tobytes() == EOC) or
+ len(v) == 0
+ ):
continue
sub_decode_path = decode_path + (name,)
try:
sub_offset,
leavemm=True,
decode_path=sub_decode_path,
- defines_by_path=defines_by_path,
+ ctx=ctx,
)
except TagMismatch:
if spec.optional:
continue
raise
- defined = defines.pop(name, None)
+ defined = get_def_by_path(ctx.get("defines", ()), sub_decode_path)
if defined is not None:
defined_by, defined_spec = defined
if issubclass(value.__class__, SequenceOf):
for i, _value in enumerate(value):
sub_sub_decode_path = sub_decode_path + (
str(i),
- decode_path_defby(defined_by),
+ DecodePathDefBy(defined_by),
)
defined_value, defined_tail = defined_spec.decode(
memoryview(bytes(_value)),
- sub_offset + value.tlen + value.llen,
+ sub_offset + (
+ (value.tlen + value.llen + value.expl_tlen + value.expl_llen)
+ if value.expled else (value.tlen + value.llen)
+ ),
leavemm=True,
decode_path=sub_sub_decode_path,
- defines_by_path=defines_by_path,
+ ctx=ctx,
)
if len(defined_tail) > 0:
raise DecodeError(
else:
defined_value, defined_tail = defined_spec.decode(
memoryview(bytes(value)),
- sub_offset + value.tlen + value.llen,
+ sub_offset + (
+ (value.tlen + value.llen + value.expl_tlen + value.expl_llen)
+ if value.expled else (value.tlen + value.llen)
+ ),
leavemm=True,
- decode_path=sub_decode_path + (decode_path_defby(defined_by),),
- defines_by_path=defines_by_path,
+ decode_path=sub_decode_path + (DecodePathDefBy(defined_by),),
+ ctx=ctx,
)
if len(defined_tail) > 0:
raise DecodeError(
"remaining data",
klass=self.__class__,
- decode_path=sub_decode_path + (decode_path_defby(defined_by),),
+ decode_path=sub_decode_path + (DecodePathDefBy(defined_by),),
offset=offset,
)
value.defined = (defined_by, defined_value)
- sub_offset += (value.expl_tlvlen if value.expled else value.tlvlen)
+ value_len = value.expl_tlvlen if value.expled else value.tlvlen
+ vlen += value_len
+ sub_offset += value_len
v = v_tail
if spec.default is not None and value == spec.default:
- # Encoded default values are not valid in DER,
- # but we allow that anyway
- continue
+ if ctx.get("strict_default_existence", False):
+ raise DecodeError(
+ "DEFAULT value met",
+ klass=self.__class__,
+ decode_path=sub_decode_path,
+ offset=sub_offset,
+ )
+ else:
+ continue
values[name] = value
- spec_defines = getattr(spec, "defines", None)
- if defines_by_path is not None and spec_defines is None:
- spec_defines = get_def_by_path(defines_by_path, sub_decode_path)
- if spec_defines is not None:
- what, schema = spec_defines
- defined = schema.get(value, None)
- if defined is not None:
- defines[what] = (value, defined)
- if len(v) > 0:
+ spec_defines = getattr(spec, "defines", ())
+ if len(spec_defines) == 0:
+ defines_by_path = ctx.get("defines_by_path", ())
+ if len(defines_by_path) > 0:
+ spec_defines = get_def_by_path(defines_by_path, sub_decode_path)
+ if spec_defines is not None and len(spec_defines) > 0:
+ for rel_path, schema in spec_defines:
+ defined = schema.get(value, None)
+ if defined is not None:
+ ctx.setdefault("defines", []).append((
+ abs_decode_path(sub_decode_path[:-1], rel_path),
+ (value, defined),
+ ))
+ if lenindef:
+ if v[:EOC_LEN].tobytes() != EOC:
+ raise DecodeError(
+ "no EOC",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ tail = v[EOC_LEN:]
+ vlen += EOC_LEN
+ elif len(v) > 0:
raise DecodeError(
"remaining data",
klass=self.__class__,
expl=self._expl,
default=self.default,
optional=self.optional,
- _decoded=(offset, llen, l),
+ _decoded=(offset, llen, vlen),
)
obj._value = values
+ obj.lenindef = lenindef
return obj, tail
def __repr__(self):
expl_tlen=self.expl_tlen if self.expled else None,
expl_llen=self.expl_llen if self.expled else None,
expl_vlen=self.expl_vlen if self.expled else None,
+ expl_lenindef=self.expl_lenindef,
+ lenindef=self.lenindef,
)
for name in self.specs:
value = self._value.get(name)
v = b"".join(raws)
return b"".join((self.tag, len_encode(len(v)), v))
- def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
+ def _decode(self, tlv, offset, decode_path, ctx, tag_only):
try:
t, tlen, lv = tag_strip(tlv)
except DecodeError as err:
decode_path=decode_path,
offset=offset,
)
+ if tag_only:
+ return
+ lenindef = False
try:
l, llen, v = len_decode(lv)
+ except LenIndefForm as err:
+ if not ctx.get("bered", False):
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ l, llen, v = 0, 1, lv[1:]
+ lenindef = True
except DecodeError as err:
raise err.__class__(
msg=err.msg,
klass=self.__class__,
offset=offset,
)
- v, tail = v[:l], v[l:]
+ if not lenindef:
+ v, tail = v[:l], v[l:]
+ vlen = 0
sub_offset = offset + tlen + llen
values = {}
specs_items = self.specs.items
while len(v) > 0:
+ if lenindef and v[:EOC_LEN].tobytes() == EOC:
+ break
for name, spec in specs_items():
+ sub_decode_path = decode_path + (name,)
try:
- value, v_tail = spec.decode(
+ spec.decode(
v,
sub_offset,
leavemm=True,
- decode_path=decode_path + (name,),
- defines_by_path=defines_by_path,
+ decode_path=sub_decode_path,
+ ctx=ctx,
+ tag_only=True,
)
except TagMismatch:
continue
- sub_offset += (
- value.expl_tlvlen if value.expled else value.tlvlen
- )
- v = v_tail
- if spec.default is None or value != spec.default: # pragma: no cover
- # SeqMixing.test_encoded_default_accepted covers that place
- values[name] = value
break
else:
raise TagMismatch(
decode_path=decode_path,
offset=offset,
)
+ value, v_tail = spec.decode(
+ v,
+ sub_offset,
+ leavemm=True,
+ decode_path=sub_decode_path,
+ ctx=ctx,
+ )
+ value_len = value.expl_tlvlen if value.expled else value.tlvlen
+ sub_offset += value_len
+ vlen += value_len
+ v = v_tail
+ if spec.default is None or value != spec.default: # pragma: no cover
+ # SeqMixing.test_encoded_default_accepted covers that place
+ values[name] = value
obj = self.__class__(
schema=self.specs,
impl=self.tag,
expl=self._expl,
default=self.default,
optional=self.optional,
- _decoded=(offset, llen, l),
+ _decoded=(offset, llen, vlen + (EOC_LEN if lenindef else 0)),
)
obj._value = values
- return obj, tail
+ if not obj.ready:
+ raise DecodeError(
+ msg="not all values are ready",
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ obj.lenindef = lenindef
+ return obj, (v[EOC_LEN:] if lenindef else tail)
class SequenceOf(Obj):
v = b"".join(self._encoded_values())
return b"".join((self.tag, len_encode(len(v)), v))
- def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
+ def _decode(self, tlv, offset, decode_path, ctx, tag_only):
try:
t, tlen, lv = tag_strip(tlv)
except DecodeError as err:
decode_path=decode_path,
offset=offset,
)
+ if tag_only:
+ return
+ lenindef = False
try:
l, llen, v = len_decode(lv)
+ except LenIndefForm as err:
+ if not ctx.get("bered", False):
+ raise err.__class__(
+ msg=err.msg,
+ klass=self.__class__,
+ decode_path=decode_path,
+ offset=offset,
+ )
+ l, llen, v = 0, 1, lv[1:]
+ lenindef = True
except DecodeError as err:
raise err.__class__(
msg=err.msg,
decode_path=decode_path,
offset=offset,
)
- v, tail = v[:l], v[l:]
+ if not lenindef:
+ v, tail = v[:l], v[l:]
+ vlen = 0
sub_offset = offset + tlen + llen
_value = []
spec = self.spec
while len(v) > 0:
+ if lenindef and v[:EOC_LEN].tobytes() == EOC:
+ break
value, v_tail = spec.decode(
v,
sub_offset,
leavemm=True,
decode_path=decode_path + (str(len(_value)),),
- defines_by_path=defines_by_path,
+ ctx=ctx,
)
- sub_offset += (value.expl_tlvlen if value.expled else value.tlvlen)
+ value_len = value.expl_tlvlen if value.expled else value.tlvlen
+ sub_offset += value_len
+ vlen += value_len
v = v_tail
_value.append(value)
obj = self.__class__(
expl=self._expl,
default=self.default,
optional=self.optional,
- _decoded=(offset, llen, l),
+ _decoded=(offset, llen, vlen),
)
- return obj, tail
+ obj.lenindef = lenindef
+ return obj, (v[EOC_LEN:] if lenindef else tail)
def __repr__(self):
return "%s[%s]" % (
expl_tlen=self.expl_tlen if self.expled else None,
expl_llen=self.expl_llen if self.expled else None,
expl_vlen=self.expl_vlen if self.expled else None,
+ expl_lenindef=self.expl_lenindef,
+ lenindef=self.lenindef,
)
for i, value in enumerate(self._value):
yield value.pps(decode_path=decode_path + (str(i),))
__slots__ = ()
schema = choice
- def pprint_any(obj, oids=None):
+ def pprint_any(obj, oids=None, with_colours=False):
def _pprint_pps(pps):
for pp in pps:
if hasattr(pp, "_fields"):
oids=oids,
with_offsets=True,
with_blob=False,
+ with_colours=with_colours,
)
for row in pp_console_blob(pp):
yield row
def main(): # pragma: no cover
import argparse
- parser = argparse.ArgumentParser(description="PyDERASN ASN.1 DER decoder")
+ parser = argparse.ArgumentParser(description="PyDERASN ASN.1 BER/DER decoder")
parser.add_argument(
"--skip",
type=int,
"--defines-by-path",
help="Python path to decoder's defines_by_path",
)
+ parser.add_argument(
+ "--nobered",
+ action='store_true',
+ help="Disallow BER encoding",
+ )
parser.add_argument(
"DERFile",
type=argparse.FileType("rb"),
pprinter = partial(pprint, big_blobs=True)
else:
schema, pprinter = generic_decoder()
- obj, tail = schema().decode(
- der,
- defines_by_path=(
- None if args.defines_by_path is None
- else obj_by_path(args.defines_by_path)
- ),
- )
- print(pprinter(obj, oids=oids))
+ ctx = {"bered": not args.nobered}
+ if args.defines_by_path is not None:
+ ctx["defines_by_path"] = obj_by_path(args.defines_by_path)
+ obj, tail = schema().decode(der, ctx=ctx)
+ print(pprinter(
+ obj,
+ oids=oids,
+ with_colours=True if environ.get("NO_COLOR") is None else False,
+ ))
if tail != b"":
print("\nTrailing data: %s" % hexenc(tail))