#!/usr/bin/env python
# coding: utf-8
# PyDERASN -- Python ASN.1 DER codec with abstract structures
-# Copyright (C) 2017 Sergey Matveev <stargrave@stargrave.org>
+# Copyright (C) 2017-2018 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
Most types in ASN.1 has specific tag for them. ``Obj.tag_default`` is
the default tag used during coding process. You can override it with
either ``IMPLICIT`` (using ``impl`` keyword argument), or
-``EXPLICIT`` one (using ``expl`` keyword argument). Both arguments takes
+``EXPLICIT`` one (using ``expl`` keyword argument). Both arguments take
raw binary string, containing that tag. You can **not** set implicit and
explicit tags simultaneously.
Implicit tag is not explicitly shown.
-Two object of the same type, but with different implicit/explicit tags
+Two objects of the same type, but with different implicit/explicit tags
are **not** equal.
-You can get objects effective tag (either default or implicited) through
+You can get object's effective tag (either default or implicited) through
``tag`` property. You can decode it using :py:func:`pyderasn.tag_decode`
function::
Common methods
______________
-All objects have ``ready`` boolean property, that tells if it is ready
-to be encoded. If that kind of action is performed on unready object,
-then :py:exc:`pyderasn.ObjNotReady` exception will be raised.
+All objects have ``ready`` boolean property, that tells if object is
+ready to be encoded. If that kind of action is performed on unready
+object, then :py:exc:`pyderasn.ObjNotReady` exception will be raised.
-All objects have ``copy()`` method, returning its copy, that can be safely
-mutated.
+All objects have ``copy()`` method, that returns their copy, that can be
+safely mutated.
.. _decoding:
When error occurs, then :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:`defines_by_path <defines_by_path_ctx>`
+* :ref:`strict_default_existence <strict_default_existence_ctx>`
+
.. _pprinting:
Pretty printing
: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
When any of those fields is automatically decoded, then ``.defined``
-attribute contains ``(OID, value)`` tuple. OID tell by which OID it was
-defined, ``value`` contains corresponding decoded value. For example
+attribute contains ``(OID, value)`` tuple. ``OID`` tells by which OID it
+was defined, ``value`` contains corresponding decoded value. For example
above, ``content_info["content"].defined == (id_signedData,
signed_data)``.
-defines_by_path kwarg
-_____________________
+.. _defines_by_path_ctx:
+
+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()}),),
),
(
(
"encapContentInfo",
"eContentType",
),
- ("eContent", {
+ ((("eContent",), {
id_cct_PKIData: PKIData(),
id_cct_PKIResponse: PKIResponse(),
- }),
+ })),
),
(
(
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``.
First function is useful for path construction when some automatic
-decoding is already done. ``any`` is used for human readability and
-means literally any value it meet -- useful for sequence and set of-s.
+decoding is already done. ``any`` means literally any value it meet --
+useful for SEQUENCE/SET OF-s.
Primitive types
---------------
Various
-------
+.. autofunction:: pyderasn.abs_decode_path
.. autofunction:: pyderasn.hexenc
.. autofunction:: pyderasn.hexdec
.. autofunction:: pyderasn.tag_encode
########################################################################
class AutoAddSlots(type):
- def __new__(cls, name, bases, _dict):
+ def __new__(mcs, name, bases, _dict):
_dict["__slots__"] = _dict.get("__slots__", ())
- return type.__new__(cls, name, bases, _dict)
+ return type.__new__(mcs, name, bases, _dict)
@add_metaclass(AutoAddSlots)
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): # 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):
"""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.
:returns: (Obj, remaining data)
"""
+ if ctx is None:
+ ctx = {}
tlv = memoryview(data)
if self._expl is None:
obj, tail = self._decode(
tlv,
offset,
decode_path=decode_path,
- defines_by_path=defines_by_path,
+ ctx=ctx,
)
else:
try:
v,
offset=offset + tlen + llen,
decode_path=decode_path,
- defines_by_path=defines_by_path,
+ ctx=ctx,
)
return obj, (tail if leavemm else tail.tobytes())
(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):
try:
t, _, lv = tag_strip(tlv)
except DecodeError as err:
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):
try:
t, _, lv = tag_strip(tlv)
except DecodeError as err:
>>> b.specs
{'nonRepudiation': 1, 'digitalSignature': 0, 'keyEncipherment': 2}
"""
- __slots__ = ("specs",)
+ __slots__ = ("specs", "defined")
tag_default = tag_encode(3)
asn1_type_name = "BIT STRING"
)
if value is None:
self._value = default
+ self.defined = None
def _bits2octets(self, bits):
if len(self.specs) > 0:
octets,
))
- def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
+ def _decode(self, tlv, offset, decode_path, ctx):
try:
t, _, lv = tag_strip(tlv)
except DecodeError as err:
expl_llen=self.expl_llen if self.expled else None,
expl_vlen=self.expl_vlen if self.expled else None,
)
+ 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),)
+ )
class OctetString(Obj):
self._value,
))
- def _decode(self, tlv, offset=0, decode_path=(), defines_by_path=None):
+ def _decode(self, tlv, offset, decode_path, ctx):
try:
t, _, lv = tag_strip(tlv)
except DecodeError as err:
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):
try:
t, _, lv = tag_strip(tlv)
except DecodeError as err:
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):
try:
t, _, lv = tag_strip(tlv)
except DecodeError as err:
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):
for choice, spec in self.specs.items():
try:
value, tail = spec.decode(
offset=offset,
leavemm=True,
decode_path=decode_path + (choice,),
- defines_by_path=defines_by_path,
+ ctx=ctx,
)
except TagMismatch:
continue
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):
try:
t, tlen, lv = tag_strip(tlv)
l, llen, v = len_decode(lv)
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
>>> tbs = TBSCertificate()
>>> tbs["version"] = Version("v2") # no need to explicitly add ``expl``
+ Assign ``None`` to remove value from sequence.
+
You can know if value exists/set in the sequence and take its value:
>>> "extnID" in ext, "extnValue" in ext, "critical" in ext
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.
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):
try:
t, tlen, lv = tag_strip(tlv)
except DecodeError as err:
v, tail = v[:l], v[l:]
sub_offset = offset + tlen + llen
values = {}
- defines = {}
for name, spec in self.specs.items():
if len(v) == 0 and spec.optional:
continue
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):
sub_offset + 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(
sub_offset + value.tlen + value.llen,
leavemm=True,
decode_path=sub_decode_path + (decode_path_defby(defined_by),),
- defines_by_path=defines_by_path,
+ ctx=ctx,
)
if len(defined_tail) > 0:
raise DecodeError(
sub_offset += (value.expl_tlvlen if value.expled else value.tlvlen)
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)
+ 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 len(v) > 0:
raise DecodeError(
"remaining data",
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):
try:
t, tlen, lv = tag_strip(tlv)
except DecodeError as err:
sub_offset,
leavemm=True,
decode_path=decode_path + (name,),
- defines_by_path=defines_by_path,
+ ctx=ctx,
)
except TagMismatch:
continue
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):
try:
t, tlen, lv = tag_strip(tlv)
except DecodeError as err:
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)
v = v_tail
def main(): # pragma: no cover
import argparse
parser = argparse.ArgumentParser(description="PyDERASN ASN.1 DER decoder")
+ parser.add_argument(
+ "--skip",
+ type=int,
+ default=0,
+ help="Skip that number of bytes from the beginning",
+ )
parser.add_argument(
"--oids",
help="Python path to dictionary with OIDs",
"--schema",
help="Python path to schema definition to use",
)
+ parser.add_argument(
+ "--defines-by-path",
+ help="Python path to decoder's defines_by_path",
+ )
parser.add_argument(
"DERFile",
type=argparse.FileType("rb"),
help="Path to DER file you want to decode",
)
args = parser.parse_args()
+ args.DERFile.seek(args.skip)
der = memoryview(args.DERFile.read())
args.DERFile.close()
oids = obj_by_path(args.oids) if args.oids else {}
pprinter = partial(pprint, big_blobs=True)
else:
schema, pprinter = generic_decoder()
- obj, tail = schema().decode(der)
+ obj, tail = schema().decode(
+ der,
+ ctx=(
+ None if args.defines_by_path is None else
+ {"defines_by_path": obj_by_path(args.defines_by_path)}
+ ),
+ )
print(pprinter(obj, oids=oids))
if tail != b"":
print("\nTrailing data: %s" % hexenc(tail))