When object is decoded, ``decoded`` property is true and you can safely
use following properties:
-* ``offset`` -- position from initial offset where object's tag is started
+* ``offset`` -- position including initial offset where object's tag starts
* ``tlen`` -- length of object's tag
* ``llen`` -- length of object's length value
* ``vlen`` -- length of object's value
_____
.. autoclass:: pyderasn.SetOf
:members: __init__
+
+Various
+-------
+
+.. autofunction:: pyderasn.hexenc
+.. autofunction:: pyderasn.hexdec
+.. autofunction:: pyderasn.tag_encode
+.. autofunction:: pyderasn.tag_decode
+.. autofunction:: pyderasn.tag_ctxp
+.. autofunction:: pyderasn.tag_ctxc
+.. autoclass:: pyderasn.Obj
"""
from codecs import getdecoder
from datetime import datetime
from math import ceil
+from six import add_metaclass
from six import binary_type
from six import byte2int
from six import indexbytes
class DecodeError(Exception):
def __init__(self, msg="", klass=None, decode_path=(), offset=0):
+ """
+ :param str msg: reason of decode failing
+ :param klass: optional exact DecodeError inherited class (like
+ :py:exc:`NotEnoughData`, :py:exc:`TagMismatch`,
+ :py:exc:`InvalidLength`)
+ :param decode_path: tuple of strings. It contains human
+ readable names of the fields through which
+ decoding process has passed
+ :param int offset: binary offset where failure happened
+ """
super(DecodeError, self).__init__()
self.msg = msg
self.klass = klass
def hexdec(data):
+ """Binary data to hexadecimal string convert
+ """
return _hexdecoder(data)[0]
def hexenc(data):
+ """Hexadecimal string to binary data convert
+ """
return _hexencoder(data)[0].decode("ascii")
def tag_encode(num, klass=TagClassUniversal, form=TagFormPrimitive):
+ """Encode tag to binary form
+
+ :param int num: tag's number
+ :param int klass: tag's class (:py:data:`pyderasn.TagClassUniversal`,
+ :py:data:`pyderasn.TagClassContext`,
+ :py:data:`pyderasn.TagClassApplication`,
+ :py:data:`pyderasn.TagClassPrivate`)
+ :param int form: tag's form (:py:data:`pyderasn.TagFormPrimitive`,
+ :py:data:`pyderasn.TagFormConstructed`)
+ """
if num < 31:
# [XX|X|.....]
return int2byte(klass | form | num)
def tag_decode(tag):
- """
- assume that data is validated
+ """Decode tag from binary form
+
+ .. warning::
+
+ No validation is performed, assuming that it has already passed.
+
+ It returns tuple with three integers, as
+ :py:func:`pyderasn.tag_encode` accepts.
"""
first_octet = byte2int(tag)
klass = first_octet & 0xC0
def tag_ctxp(num):
+ """Create CONTEXT PRIMITIVE tag
+ """
return tag_encode(num=num, klass=TagClassContext, form=TagFormPrimitive)
def tag_ctxc(num):
+ """Create CONTEXT CONSTRUCTED tag
+ """
return tag_encode(num=num, klass=TagClassContext, form=TagFormConstructed)
# Base class
########################################################################
+class AutoAddSlots(type):
+ def __new__(cls, name, bases, _dict):
+ _dict["__slots__"] = _dict.get("__slots__", ())
+ return type.__new__(cls, name, bases, _dict)
+
+
+@add_metaclass(AutoAddSlots)
class Obj(object):
"""Common ASN.1 object class
+
+ All ASN.1 types are inherited from it. It has metaclass that
+ automatically adds ``__slots__`` to all inherited classes.
"""
__slots__ = (
"tag",
optional=False,
_decoded=(0, 0, 0),
):
- if impl is None:
- self.tag = getattr(self, "impl", self.tag_default)
- else:
- self.tag = impl
+ self.tag = getattr(self, "impl", self.tag_default) if impl is None else impl
self._expl = getattr(self, "expl", None) if expl is None else expl
if self.tag != self.tag_default and self._expl is not None:
raise ValueError(
@property
def ready(self): # pragma: no cover
+ """Is object ready to be encoded?
+ """
raise NotImplementedError()
def _assert_ready(self):
@property
def decoded(self):
- return self.llen > 0
+ """Is object decoded?
+ """
+ return (self.llen + self.vlen) > 0
def copy(self): # pragma: no cover
+ """Make a copy of object, safe to be mutated
+ """
raise NotImplementedError()
@property
def __str__(self): # pragma: no cover
return self.__bytes__() if PY2 else self.__unicode__()
+ def __ne__(self, their):
+ return not(self == their)
+
+ def __gt__(self, their): # pragma: no cover
+ return not(self < their)
+
+ def __le__(self, their): # pragma: no cover
+ return (self == their) or (self < their)
+
+ def __ge__(self, their): # pragma: no cover
+ return (self == their) or (self > their)
+
def _encode(self): # pragma: no cover
raise NotImplementedError()
return b"".join((self._expl, len_encode(len(raw)), raw))
def decode(self, data, offset=0, leavemm=False, decode_path=()):
+ """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
+ :returns: (Obj, remaining data)
+ """
tlv = memoryview(data)
if self._expl is None:
obj, tail = self._decode(
def pprint(obj, oids=None, big_blobs=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 big_blobs: if large binary objects are met (like OctetString
+ values), do we need to print them too, on separate
+ lines
+ """
def _pprint_pps(pps):
for pp in pps:
if hasattr(pp, "_fields"):
True
>>> bool(b)
True
- >>> Boolean(optional=True)
- BOOLEAN OPTIONAL
- >>> Boolean(impl=tag_ctxp(1), default=False)
- [1] BOOLEAN False OPTIONAL DEFAULT
"""
__slots__ = ()
tag_default = tag_encode(1)
True
>>> int(b)
-123
- >>> Integer(optional=True)
- INTEGER OPTIONAL
- >>> Integer(impl=tag_ctxp(1), default=123)
- [1] INTEGER 123 OPTIONAL DEFAULT
>>> Integer(2, bounds=(1, 3))
INTEGER 2
self._value = value
specs = getattr(self, "schema", {}) if _specs is None else _specs
self.specs = specs if isinstance(specs, dict) else dict(specs)
- if bounds is None:
- self._bound_min, self._bound_max = getattr(
- self,
- "bounds",
- (float("-inf"), float("+inf")),
- )
- else:
- self._bound_min, self._bound_max = bounds
+ self._bound_min, self._bound_max = getattr(
+ self,
+ "bounds",
+ (float("-inf"), float("+inf")),
+ ) if bounds is None else bounds
if value is not None:
self._value = self._value_sanitize(value)
if default is not None:
)
def __lt__(self, their):
- return self._value < their
-
- def __gt__(self, their):
- return self._value > their
+ return self._value < their._value
@property
def named(self):
def copy(self):
obj = self.__class__(_specs=self.specs)
- obj._value = self._value
+ value = self._value
+ if value is not None:
+ value = (value[0], value[1])
+ obj._value = value
obj.tag = self.tag
obj._expl = self._expl
obj.default = self.default
_decoded,
)
self._value = value
- if bounds is None:
- self._bound_min, self._bound_max = getattr(
- self,
- "bounds",
- (0, float("+inf")),
- )
- else:
- self._bound_min, self._bound_max = bounds
+ self._bound_min, self._bound_max = getattr(
+ self,
+ "bounds",
+ (0, float("+inf")),
+ ) if bounds is None else bounds
if value is not None:
self._value = self._value_sanitize(value)
if default is not None:
self._expl == their._expl
)
+ def __lt__(self, their):
+ return self._value < their._value
+
def __call__(
self,
value=None,
)
def __lt__(self, their):
- return self._value < their
-
- def __gt__(self, their):
- return self._value > their
+ return self._value < their._value
def __call__(
self,
asn1_type_name = "IA5"
+LEN_YYMMDDHHMMSSZ = len("YYMMDDHHMMSSZ")
+LEN_YYYYMMDDHHMMSSDMZ = len("YYYYMMDDHHMMSSDMZ")
+LEN_YYYYMMDDHHMMSSZ = len("YYYYMMDDHHMMSSZ")
+
+
class UTCTime(CommonString):
"""``UTCTime`` datetime type
return value.strftime(self.fmt).encode("ascii")
if isinstance(value, binary_type):
value_decoded = value.decode("ascii")
- if len(value_decoded) == 2 + 2 + 2 + 2 + 2 + 2 + 1:
+ if len(value_decoded) == LEN_YYMMDDHHMMSSZ:
try:
datetime.strptime(value_decoded, self.fmt)
except ValueError:
).encode("ascii")
if isinstance(value, binary_type):
value_decoded = value.decode("ascii")
- if len(value_decoded) == 4 + 2 + 2 + 2 + 2 + 2 + 1:
+ if len(value_decoded) == LEN_YYYYMMDDHHMMSSZ:
try:
datetime.strptime(value_decoded, self.fmt)
except ValueError:
"invalid GeneralizedTime (without ms) format",
)
return value
- elif len(value_decoded) >= 4 + 2 + 2 + 2 + 2 + 2 + 1 + 1 + 1:
+ elif len(value_decoded) >= LEN_YYYYMMDDHHMMSSDMZ:
try:
datetime.strptime(value_decoded, self.fmt_ms)
except ValueError:
def todatetime(self):
value = self._value.decode("ascii")
- if len(value) == 4 + 2 + 2 + 2 + 2 + 2 + 1:
+ if len(value) == LEN_YYYYMMDDHHMMSSZ:
return datetime.strptime(value, self.fmt)
return datetime.strptime(value, self.fmt_ms)
########################################################################
class Sequence(Obj):
+ """``SEQUENCE`` structure type
+
+ You have to make specification of sequence::
+
+ class Extension(Sequence):
+ schema = (
+ ("extnID", ObjectIdentifier()),
+ ("critical", Boolean(default=False)),
+ ("extnValue", OctetString()),
+ )
+
+ Then, you can work with it as with dictionary.
+
+ >>> ext = Extension()
+ >>> Extension().specs
+ OrderedDict([
+ ('extnID', OBJECT IDENTIFIER),
+ ('critical', BOOLEAN False OPTIONAL DEFAULT),
+ ('extnValue', OCTET STRING),
+ ])
+ >>> ext["extnID"] = "1.2.3"
+ Traceback (most recent call last):
+ 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:
+
+ >>> ext.ready
+ False
+ >>> ext.encode()
+ Traceback (most recent call last):
+ pyderasn.ObjNotReady: object is not ready: extnValue
+ >>> ext["extnValue"] = OctetString(b"foobar")
+ >>> ext.ready
+ True
+
+ Value you want to assign, must have the same **type** as in
+ corresponding specification, but it can have different tags,
+ optional/default attributes -- they will be taken from specification
+ automatically::
+
+ class TBSCertificate(Sequence):
+ schema = (
+ ("version", Version(expl=tag_ctxc(0), default="v1")),
+ [...]
+
+ >>> 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:
+
+ >>> "extnID" in ext, "extnValue" in ext, "critical" in ext
+ (True, True, False)
+ >>> ext["extnID"]
+ OBJECT IDENTIFIER 1.2.3
+
+ But pay attention that if value has default, then it won't be (not
+ in) in the sequence (because ``DEFAULT`` must not be encoded in
+ DER), but you can read its value:
+
+ >>> "critical" in ext, ext["critical"]
+ (False, BOOLEAN False)
+ >>> ext["critical"] = Boolean(True)
+ >>> "critical" in ext, ext["critical"]
+ (True, BOOLEAN True)
+
+ All defaulted values are always optional.
+
+ .. 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
+ result in different binary representation (validly without
+ defaulted value inside).
+
+ Two sequences are equal if they have equal specification (schema),
+ implicit/explicit tagging and the same values.
+ """
__slots__ = ("specs",)
tag_default = tag_encode(form=TagFormConstructed, num=16)
asn1_type_name = "SEQUENCE"
class Set(Sequence):
+ """``SET`` structure type
+
+ Its usage is identical to :py:class:`pyderasn.Sequence`.
+ """
__slots__ = ()
tag_default = tag_encode(form=TagFormConstructed, num=17)
asn1_type_name = "SET"
class SequenceOf(Obj):
+ """``SEQUENCE OF`` sequence type
+
+ For that kind of type you must specify the object it will carry on
+ (bounds are for example here, not required)::
+
+ class Ints(SequenceOf):
+ schema = Integer()
+ bounds = (0, 2)
+
+ >>> ints = Ints()
+ >>> ints.append(Integer(123))
+ >>> ints.append(Integer(234))
+ >>> ints
+ Ints SEQUENCE OF[INTEGER 123, INTEGER 234]
+ >>> [int(i) for i in ints]
+ [123, 234]
+ >>> ints.append(Integer(345))
+ Traceback (most recent call last):
+ pyderasn.BoundsError: unsatisfied bounds: 0 <= 3 <= 2
+ >>> ints[1]
+ INTEGER 234
+ >>> ints[1] = Integer(345)
+ >>> ints
+ Ints SEQUENCE OF[INTEGER 123, INTEGER 345]
+
+ Also you can initialize sequence with preinitialized values:
+
+ >>> ints = Ints([Integer(123), Integer(234)])
+ """
__slots__ = ("spec", "_bound_min", "_bound_max")
tag_default = tag_encode(form=TagFormConstructed, num=16)
asn1_type_name = "SEQUENCE OF"
if schema is None:
raise ValueError("schema must be specified")
self.spec = schema
- if bounds is None:
- self._bound_min, self._bound_max = getattr(
- self,
- "bounds",
- (0, float("+inf")),
- )
- else:
- self._bound_min, self._bound_max = bounds
+ self._bound_min, self._bound_max = getattr(
+ self,
+ "bounds",
+ (0, float("+inf")),
+ ) if bounds is None else bounds
self._value = []
if value is not None:
self._value = self._value_sanitize(value)
class SetOf(SequenceOf):
+ """``SET OF`` sequence type
+
+ Its usage is identical to :py:class:`pyderasn.SequenceOf`.
+ """
__slots__ = ()
tag_default = tag_encode(form=TagFormConstructed, num=17)
asn1_type_name = "SET OF"
parser.add_argument(
"DERFile",
type=argparse.FileType("rb"),
- help="Python path to schema definition to use",
+ help="Path to DER file you want to decode",
)
args = parser.parse_args()
der = memoryview(args.DERFile.read())