blob: 933f8caae65a2a12ef5a25f1950c38ec638ebf1b [file] [log] [blame]
# coding: utf-8
"""
ASN.1 type classes for universal types. Exports the following items:
- load()
- Any()
- Asn1Value()
- BitString()
- BMPString()
- Boolean()
- CharacterString()
- Choice()
- EmbeddedPdv()
- Enumerated()
- GeneralizedTime()
- GeneralString()
- GraphicString()
- IA5String()
- InstanceOf()
- Integer()
- IntegerBitString()
- IntegerOctetString()
- Null()
- NumericString()
- ObjectDescriptor()
- ObjectIdentifier()
- OctetBitString()
- OctetString()
- PrintableString()
- Real()
- RelativeOid()
- Sequence()
- SequenceOf()
- Set()
- SetOf()
- TeletexString()
- UniversalString()
- UTCTime()
- UTF8String()
- VideotexString()
- VisibleString()
- VOID
- Void()
Other type classes are defined that help compose the types listed above.
"""
from __future__ import unicode_literals, division, absolute_import, print_function
from datetime import datetime, timedelta
from fractions import Fraction
import binascii
import copy
import math
import re
import sys
from . import _teletex_codec
from ._errors import unwrap
from ._ordereddict import OrderedDict
from ._types import type_name, str_cls, byte_cls, int_types, chr_cls
from .parser import _parse, _dump_header
from .util import int_to_bytes, int_from_bytes, timezone, extended_datetime, create_timezone, utc_with_dst
if sys.version_info <= (3,):
from cStringIO import StringIO as BytesIO
range = xrange # noqa
_PY2 = True
else:
from io import BytesIO
_PY2 = False
_teletex_codec.register()
CLASS_NUM_TO_NAME_MAP = {
0: 'universal',
1: 'application',
2: 'context',
3: 'private',
}
CLASS_NAME_TO_NUM_MAP = {
'universal': 0,
'application': 1,
'context': 2,
'private': 3,
0: 0,
1: 1,
2: 2,
3: 3,
}
METHOD_NUM_TO_NAME_MAP = {
0: 'primitive',
1: 'constructed',
}
_OID_RE = re.compile(r'^\d+(\.\d+)*$')
# A global tracker to ensure that _setup() is called for every class, even
# if is has been called for a parent class. This allows different _fields
# definitions for child classes. Without such a construct, the child classes
# would just see the parent class attributes and would use them.
_SETUP_CLASSES = {}
def load(encoded_data, strict=False):
"""
Loads a BER/DER-encoded byte string and construct a universal object based
on the tag value:
- 1: Boolean
- 2: Integer
- 3: BitString
- 4: OctetString
- 5: Null
- 6: ObjectIdentifier
- 7: ObjectDescriptor
- 8: InstanceOf
- 9: Real
- 10: Enumerated
- 11: EmbeddedPdv
- 12: UTF8String
- 13: RelativeOid
- 16: Sequence,
- 17: Set
- 18: NumericString
- 19: PrintableString
- 20: TeletexString
- 21: VideotexString
- 22: IA5String
- 23: UTCTime
- 24: GeneralizedTime
- 25: GraphicString
- 26: VisibleString
- 27: GeneralString
- 28: UniversalString
- 29: CharacterString
- 30: BMPString
:param encoded_data:
A byte string of BER or DER-encoded data
:param strict:
A boolean indicating if trailing data should be forbidden - if so, a
ValueError will be raised when trailing data exists
:raises:
ValueError - when strict is True and trailing data is present
ValueError - when the encoded value tag a tag other than listed above
ValueError - when the ASN.1 header length is longer than the data
TypeError - when encoded_data is not a byte string
:return:
An instance of the one of the universal classes
"""
return Asn1Value.load(encoded_data, strict=strict)
class Asn1Value(object):
"""
The basis of all ASN.1 values
"""
# The integer 0 for primitive, 1 for constructed
method = None
# An integer 0 through 3 - see CLASS_NUM_TO_NAME_MAP for value
class_ = None
# An integer 1 or greater indicating the tag number
tag = None
# An alternate tag allowed for this type - used for handling broken
# structures where a string value is encoded using an incorrect tag
_bad_tag = None
# If the value has been implicitly tagged
implicit = False
# If explicitly tagged, a tuple of 2-element tuples containing the
# class int and tag int, from innermost to outermost
explicit = None
# The BER/DER header bytes
_header = None
# Raw encoded value bytes not including class, method, tag, length header
contents = None
# The BER/DER trailer bytes
_trailer = b''
# The native python representation of the value - this is not used by
# some classes since they utilize _bytes or _unicode
_native = None
@classmethod
def load(cls, encoded_data, strict=False, **kwargs):
"""
Loads a BER/DER-encoded byte string using the current class as the spec
:param encoded_data:
A byte string of BER or DER-encoded data
:param strict:
A boolean indicating if trailing data should be forbidden - if so, a
ValueError will be raised when trailing data exists
:return:
An instance of the current class
"""
if not isinstance(encoded_data, byte_cls):
raise TypeError('encoded_data must be a byte string, not %s' % type_name(encoded_data))
spec = None
if cls.tag is not None:
spec = cls
value, _ = _parse_build(encoded_data, spec=spec, spec_params=kwargs, strict=strict)
return value
def __init__(self, explicit=None, implicit=None, no_explicit=False, tag_type=None, class_=None, tag=None,
optional=None, default=None, contents=None, method=None):
"""
The optional parameter is not used, but rather included so we don't
have to delete it from the parameter dictionary when passing as keyword
args
:param explicit:
An int tag number for explicit tagging, or a 2-element tuple of
class and tag.
:param implicit:
An int tag number for implicit tagging, or a 2-element tuple of
class and tag.
:param no_explicit:
If explicit tagging info should be removed from this instance.
Used internally to allow contructing the underlying value that
has been wrapped in an explicit tag.
:param tag_type:
None for normal values, or one of "implicit", "explicit" for tagged
values. Deprecated in favor of explicit and implicit params.
:param class_:
The class for the value - defaults to "universal" if tag_type is
None, otherwise defaults to "context". Valid values include:
- "universal"
- "application"
- "context"
- "private"
Deprecated in favor of explicit and implicit params.
:param tag:
The integer tag to override - usually this is used with tag_type or
class_. Deprecated in favor of explicit and implicit params.
:param optional:
Dummy parameter that allows "optional" key in spec param dicts
:param default:
The default value to use if the value is currently None
:param contents:
A byte string of the encoded contents of the value
:param method:
The method for the value - no default value since this is
normally set on a class. Valid values include:
- "primitive" or 0
- "constructed" or 1
:raises:
ValueError - when implicit, explicit, tag_type, class_ or tag are invalid values
"""
try:
if self.__class__ not in _SETUP_CLASSES:
cls = self.__class__
# Allow explicit to be specified as a simple 2-element tuple
# instead of requiring the user make a nested tuple
if cls.explicit is not None and isinstance(cls.explicit[0], int_types):
cls.explicit = (cls.explicit, )
if hasattr(cls, '_setup'):
self._setup()
_SETUP_CLASSES[cls] = True
# Normalize tagging values
if explicit is not None:
if isinstance(explicit, int_types):
if class_ is None:
class_ = 'context'
explicit = (class_, explicit)
# Prevent both explicit and tag_type == 'explicit'
if tag_type == 'explicit':
tag_type = None
tag = None
if implicit is not None:
if isinstance(implicit, int_types):
if class_ is None:
class_ = 'context'
implicit = (class_, implicit)
# Prevent both implicit and tag_type == 'implicit'
if tag_type == 'implicit':
tag_type = None
tag = None
# Convert old tag_type API to explicit/implicit params
if tag_type is not None:
if class_ is None:
class_ = 'context'
if tag_type == 'explicit':
explicit = (class_, tag)
elif tag_type == 'implicit':
implicit = (class_, tag)
else:
raise ValueError(unwrap(
'''
tag_type must be one of "implicit", "explicit", not %s
''',
repr(tag_type)
))
if explicit is not None:
# Ensure we have a tuple of 2-element tuples
if len(explicit) == 2 and isinstance(explicit[1], int_types):
explicit = (explicit, )
for class_, tag in explicit:
invalid_class = None
if isinstance(class_, int_types):
if class_ not in CLASS_NUM_TO_NAME_MAP:
invalid_class = class_
else:
if class_ not in CLASS_NAME_TO_NUM_MAP:
invalid_class = class_
class_ = CLASS_NAME_TO_NUM_MAP[class_]
if invalid_class is not None:
raise ValueError(unwrap(
'''
explicit class must be one of "universal", "application",
"context", "private", not %s
''',
repr(invalid_class)
))
if tag is not None:
if not isinstance(tag, int_types):
raise TypeError(unwrap(
'''
explicit tag must be an integer, not %s
''',
type_name(tag)
))
if self.explicit is None:
self.explicit = ((class_, tag), )
else:
self.explicit = self.explicit + ((class_, tag), )
elif implicit is not None:
class_, tag = implicit
if class_ not in CLASS_NAME_TO_NUM_MAP:
raise ValueError(unwrap(
'''
implicit class must be one of "universal", "application",
"context", "private", not %s
''',
repr(class_)
))
if tag is not None:
if not isinstance(tag, int_types):
raise TypeError(unwrap(
'''
implicit tag must be an integer, not %s
''',
type_name(tag)
))
self.class_ = CLASS_NAME_TO_NUM_MAP[class_]
self.tag = tag
self.implicit = True
else:
if class_ is not None:
if class_ not in CLASS_NAME_TO_NUM_MAP:
raise ValueError(unwrap(
'''
class_ must be one of "universal", "application",
"context", "private", not %s
''',
repr(class_)
))
self.class_ = CLASS_NAME_TO_NUM_MAP[class_]
if self.class_ is None:
self.class_ = 0
if tag is not None:
self.tag = tag
if method is not None:
if method not in set(["primitive", 0, "constructed", 1]):
raise ValueError(unwrap(
'''
method must be one of "primitive" or "constructed",
not %s
''',
repr(method)
))
if method == "primitive":
method = 0
elif method == "constructed":
method = 1
self.method = method
if no_explicit:
self.explicit = None
if contents is not None:
self.contents = contents
elif default is not None:
self.set(default)
except (ValueError, TypeError) as e:
args = e.args[1:]
e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args
raise e
def __str__(self):
"""
Since str is different in Python 2 and 3, this calls the appropriate
method, __unicode__() or __bytes__()
:return:
A unicode string
"""
if _PY2:
return self.__bytes__()
else:
return self.__unicode__()
def __repr__(self):
"""
:return:
A unicode string
"""
if _PY2:
return '<%s %s b%s>' % (type_name(self), id(self), repr(self.dump()))
else:
return '<%s %s %s>' % (type_name(self), id(self), repr(self.dump()))
def __bytes__(self):
"""
A fall-back method for print() in Python 2
:return:
A byte string of the output of repr()
"""
return self.__repr__().encode('utf-8')
def __unicode__(self):
"""
A fall-back method for print() in Python 3
:return:
A unicode string of the output of repr()
"""
return self.__repr__()
def _new_instance(self):
"""
Constructs a new copy of the current object, preserving any tagging
:return:
An Asn1Value object
"""
new_obj = self.__class__()
new_obj.class_ = self.class_
new_obj.tag = self.tag
new_obj.implicit = self.implicit
new_obj.explicit = self.explicit
return new_obj
def __copy__(self):
"""
Implements the copy.copy() interface
:return:
A new shallow copy of the current Asn1Value object
"""
new_obj = self._new_instance()
new_obj._copy(self, copy.copy)
return new_obj
def __deepcopy__(self, memo):
"""
Implements the copy.deepcopy() interface
:param memo:
A dict for memoization
:return:
A new deep copy of the current Asn1Value object
"""
new_obj = self._new_instance()
memo[id(self)] = new_obj
new_obj._copy(self, copy.deepcopy)
return new_obj
def copy(self):
"""
Copies the object, preserving any special tagging from it
:return:
An Asn1Value object
"""
return copy.deepcopy(self)
def retag(self, tagging, tag=None):
"""
Copies the object, applying a new tagging to it
:param tagging:
A dict containing the keys "explicit" and "implicit". Legacy
API allows a unicode string of "implicit" or "explicit".
:param tag:
A integer tag number. Only used when tagging is a unicode string.
:return:
An Asn1Value object
"""
# This is required to preserve the old API
if not isinstance(tagging, dict):
tagging = {tagging: tag}
new_obj = self.__class__(explicit=tagging.get('explicit'), implicit=tagging.get('implicit'))
new_obj._copy(self, copy.deepcopy)
return new_obj
def untag(self):
"""
Copies the object, removing any special tagging from it
:return:
An Asn1Value object
"""
new_obj = self.__class__()
new_obj._copy(self, copy.deepcopy)
return new_obj
def _copy(self, other, copy_func):
"""
Copies the contents of another Asn1Value object to itself
:param object:
Another instance of the same class
:param copy_func:
An reference of copy.copy() or copy.deepcopy() to use when copying
lists, dicts and objects
"""
if self.__class__ != other.__class__:
raise TypeError(unwrap(
'''
Can not copy values from %s object to %s object
''',
type_name(other),
type_name(self)
))
self.contents = other.contents
self._native = copy_func(other._native)
def debug(self, nest_level=1):
"""
Show the binary data and parsed data in a tree structure
"""
prefix = ' ' * nest_level
# This interacts with Any and moves the tag, implicit, explicit, _header,
# contents, _footer to the parsed value so duplicate data isn't present
has_parsed = hasattr(self, 'parsed')
_basic_debug(prefix, self)
if has_parsed:
self.parsed.debug(nest_level + 2)
elif hasattr(self, 'chosen'):
self.chosen.debug(nest_level + 2)
else:
if _PY2 and isinstance(self.native, byte_cls):
print('%s Native: b%s' % (prefix, repr(self.native)))
else:
print('%s Native: %s' % (prefix, self.native))
def dump(self, force=False):
"""
Encodes the value using DER
:param force:
If the encoded contents already exist, clear them and regenerate
to ensure they are in DER format instead of BER format
:return:
A byte string of the DER-encoded value
"""
contents = self.contents
# If the length is indefinite, force the re-encoding
if self._header is not None and self._header[-1:] == b'\x80':
force = True
if self._header is None or force:
if isinstance(self, Constructable) and self._indefinite:
self.method = 0
header = _dump_header(self.class_, self.method, self.tag, self.contents)
if self.explicit is not None:
for class_, tag in self.explicit:
header = _dump_header(class_, 1, tag, header + self.contents) + header
self._header = header
self._trailer = b''
return self._header + contents + self._trailer
class ValueMap():
"""
Basic functionality that allows for mapping values from ints or OIDs to
python unicode strings
"""
# A dict from primitive value (int or OID) to unicode string. This needs
# to be defined in the source code
_map = None
# A dict from unicode string to int/OID. This is automatically generated
# from _map the first time it is needed
_reverse_map = None
def _setup(self):
"""
Generates _reverse_map from _map
"""
cls = self.__class__
if cls._map is None or cls._reverse_map is not None:
return
cls._reverse_map = {}
for key, value in cls._map.items():
cls._reverse_map[value] = key
class Castable(object):
"""
A mixin to handle converting an object between different classes that
represent the same encoded value, but with different rules for converting
to and from native Python values
"""
def cast(self, other_class):
"""
Converts the current object into an object of a different class. The
new class must use the ASN.1 encoding for the value.
:param other_class:
The class to instantiate the new object from
:return:
An instance of the type other_class
"""
if other_class.tag != self.__class__.tag:
raise TypeError(unwrap(
'''
Can not covert a value from %s object to %s object since they
use different tags: %d versus %d
''',
type_name(other_class),
type_name(self),
other_class.tag,
self.__class__.tag
))
new_obj = other_class()
new_obj.class_ = self.class_
new_obj.implicit = self.implicit
new_obj.explicit = self.explicit
new_obj._header = self._header
new_obj.contents = self.contents
new_obj._trailer = self._trailer
if isinstance(self, Constructable):
new_obj.method = self.method
new_obj._indefinite = self._indefinite
return new_obj
class Constructable(object):
"""
A mixin to handle string types that may be constructed from chunks
contained within an indefinite length BER-encoded container
"""
# Instance attribute indicating if an object was indefinite
# length when parsed - affects parsing and dumping
_indefinite = False
def _merge_chunks(self):
"""
:return:
A concatenation of the native values of the contained chunks
"""
if not self._indefinite:
return self._as_chunk()
pointer = 0
contents_len = len(self.contents)
output = None
while pointer < contents_len:
# We pass the current class as the spec so content semantics are preserved
sub_value, pointer = _parse_build(self.contents, pointer, spec=self.__class__)
if output is None:
output = sub_value._merge_chunks()
else:
output += sub_value._merge_chunks()
if output is None:
return self._as_chunk()
return output
def _as_chunk(self):
"""
A method to return a chunk of data that can be combined for
constructed method values
:return:
A native Python value that can be added together. Examples include
byte strings, unicode strings or tuples.
"""
return self.contents
def _setable_native(self):
"""
Returns a native value that can be round-tripped into .set(), to
result in a DER encoding. This differs from .native in that .native
is designed for the end use, and may account for the fact that the
merged value is further parsed as ASN.1, such as in the case of
ParsableOctetString() and ParsableOctetBitString().
:return:
A python value that is valid to pass to .set()
"""
return self.native
def _copy(self, other, copy_func):
"""
Copies the contents of another Constructable object to itself
:param object:
Another instance of the same class
:param copy_func:
An reference of copy.copy() or copy.deepcopy() to use when copying
lists, dicts and objects
"""
super(Constructable, self)._copy(other, copy_func)
# We really don't want to dump BER encodings, so if we see an
# indefinite encoding, let's re-encode it
if other._indefinite:
self.set(other._setable_native())
class Void(Asn1Value):
"""
A representation of an optional value that is not present. Has .native
property and .dump() method to be compatible with other value classes.
"""
contents = b''
def __eq__(self, other):
"""
:param other:
The other Primitive to compare to
:return:
A boolean
"""
return other.__class__ == self.__class__
def __nonzero__(self):
return False
def __len__(self):
return 0
def __iter__(self):
return iter(())
@property
def native(self):
"""
The native Python datatype representation of this value
:return:
None
"""
return None
def dump(self, force=False):
"""
Encodes the value using DER
:param force:
If the encoded contents already exist, clear them and regenerate
to ensure they are in DER format instead of BER format
:return:
A byte string of the DER-encoded value
"""
return b''
VOID = Void()
class Any(Asn1Value):
"""
A value class that can contain any value, and allows for easy parsing of
the underlying encoded value using a spec. This is normally contained in
a Structure that has an ObjectIdentifier field and _oid_pair and _oid_specs
defined.
"""
# The parsed value object
_parsed = None
def __init__(self, value=None, **kwargs):
"""
Sets the value of the object before passing to Asn1Value.__init__()
:param value:
An Asn1Value object that will be set as the parsed value
"""
Asn1Value.__init__(self, **kwargs)
try:
if value is not None:
if not isinstance(value, Asn1Value):
raise TypeError(unwrap(
'''
value must be an instance of Asn1Value, not %s
''',
type_name(value)
))
self._parsed = (value, value.__class__, None)
self.contents = value.dump()
except (ValueError, TypeError) as e:
args = e.args[1:]
e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args
raise e
@property
def native(self):
"""
The native Python datatype representation of this value
:return:
The .native value from the parsed value object
"""
if self._parsed is None:
self.parse()
return self._parsed[0].native
@property
def parsed(self):
"""
Returns the parsed object from .parse()
:return:
The object returned by .parse()
"""
if self._parsed is None:
self.parse()
return self._parsed[0]
def parse(self, spec=None, spec_params=None):
"""
Parses the contents generically, or using a spec with optional params
:param spec:
A class derived from Asn1Value that defines what class_ and tag the
value should have, and the semantics of the encoded value. The
return value will be of this type. If omitted, the encoded value
will be decoded using the standard universal tag based on the
encoded tag number.
:param spec_params:
A dict of params to pass to the spec object
:return:
An object of the type spec, or if not present, a child of Asn1Value
"""
if self._parsed is None or self._parsed[1:3] != (spec, spec_params):
try:
passed_params = spec_params or {}
_tag_type_to_explicit_implicit(passed_params)
if self.explicit is not None:
if 'explicit' in passed_params:
passed_params['explicit'] = self.explicit + passed_params['explicit']
else:
passed_params['explicit'] = self.explicit
contents = self._header + self.contents + self._trailer
parsed_value, _ = _parse_build(
contents,
spec=spec,
spec_params=passed_params
)
self._parsed = (parsed_value, spec, spec_params)
# Once we've parsed the Any value, clear any attributes from this object
# since they are now duplicate
self.tag = None
self.explicit = None
self.implicit = False
self._header = b''
self.contents = contents
self._trailer = b''
except (ValueError, TypeError) as e:
args = e.args[1:]
e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args
raise e
return self._parsed[0]
def _copy(self, other, copy_func):
"""
Copies the contents of another Any object to itself
:param object:
Another instance of the same class
:param copy_func:
An reference of copy.copy() or copy.deepcopy() to use when copying
lists, dicts and objects
"""
super(Any, self)._copy(other, copy_func)
self._parsed = copy_func(other._parsed)
def dump(self, force=False):
"""
Encodes the value using DER
:param force:
If the encoded contents already exist, clear them and regenerate
to ensure they are in DER format instead of BER format
:return:
A byte string of the DER-encoded value
"""
if self._parsed is None:
self.parse()
return self._parsed[0].dump(force=force)
class Choice(Asn1Value):
"""
A class to handle when a value may be one of several options
"""
# The index in _alternatives of the validated alternative
_choice = None
# The name of the chosen alternative
_name = None
# The Asn1Value object for the chosen alternative
_parsed = None
# Choice overrides .contents to be a property so that the code expecting
# the .contents attribute will get the .contents of the chosen alternative
_contents = None
# A list of tuples in one of the following forms.
#
# Option 1, a unicode string field name and a value class
#
# ("name", Asn1ValueClass)
#
# Option 2, same as Option 1, but with a dict of class params
#
# ("name", Asn1ValueClass, {'explicit': 5})
_alternatives = None
# A dict that maps tuples of (class_, tag) to an index in _alternatives
_id_map = None
# A dict that maps alternative names to an index in _alternatives
_name_map = None
@classmethod
def load(cls, encoded_data, strict=False, **kwargs):
"""
Loads a BER/DER-encoded byte string using the current class as the spec
:param encoded_data:
A byte string of BER or DER encoded data
:param strict:
A boolean indicating if trailing data should be forbidden - if so, a
ValueError will be raised when trailing data exists
:return:
A instance of the current class
"""
if not isinstance(encoded_data, byte_cls):
raise TypeError('encoded_data must be a byte string, not %s' % type_name(encoded_data))
value, _ = _parse_build(encoded_data, spec=cls, spec_params=kwargs, strict=strict)
return value
def _setup(self):
"""
Generates _id_map from _alternatives to allow validating contents
"""
cls = self.__class__
cls._id_map = {}
cls._name_map = {}
for index, info in enumerate(cls._alternatives):
if len(info) < 3:
info = info + ({},)
cls._alternatives[index] = info
id_ = _build_id_tuple(info[2], info[1])
cls._id_map[id_] = index
cls._name_map[info[0]] = index
def __init__(self, name=None, value=None, **kwargs):
"""
Checks to ensure implicit tagging is not being used since it is
incompatible with Choice, then forwards on to Asn1Value.__init__()
:param name:
The name of the alternative to be set - used with value.
Alternatively this may be a dict with a single key being the name
and the value being the value, or a two-element tuple of the name
and the value.
:param value:
The alternative value to set - used with name
:raises:
ValueError - when implicit param is passed (or legacy tag_type param is "implicit")
"""
_tag_type_to_explicit_implicit(kwargs)
Asn1Value.__init__(self, **kwargs)
try:
if kwargs.get('implicit') is not None:
raise ValueError(unwrap(
'''
The Choice type can not be implicitly tagged even if in an
implicit module - due to its nature any tagging must be
explicit
'''
))
if name is not None:
if isinstance(name, dict):
if len(name) != 1:
raise ValueError(unwrap(
'''
When passing a dict as the "name" argument to %s,
it must have a single key/value - however %d were
present
''',
type_name(self),
len(name)
))
name, value = list(name.items())[0]
if isinstance(name, tuple):
if len(name) != 2:
raise ValueError(unwrap(
'''
When passing a tuple as the "name" argument to %s,
it must have two elements, the name and value -
however %d were present
''',
type_name(self),
len(name)
))
value = name[1]
name = name[0]
if name not in self._name_map:
raise ValueError(unwrap(
'''
The name specified, "%s", is not a valid alternative
for %s
''',
name,
type_name(self)
))
self._choice = self._name_map[name]
_, spec, params = self._alternatives[self._choice]
if not isinstance(value, spec):
value = spec(value, **params)
else:
value = _fix_tagging(value, params)
self._parsed = value
except (ValueError, TypeError) as e:
args = e.args[1:]
e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args
raise e
@property
def contents(self):
"""
:return:
A byte string of the DER-encoded contents of the chosen alternative
"""
if self._parsed is not None:
return self._parsed.contents
return self._contents
@contents.setter
def contents(self, value):
"""
:param value:
A byte string of the DER-encoded contents of the chosen alternative
"""
self._contents = value
@property
def name(self):
"""
:return:
A unicode string of the field name of the chosen alternative
"""
if not self._name:
self._name = self._alternatives[self._choice][0]
return self._name
def parse(self):
"""
Parses the detected alternative
:return:
An Asn1Value object of the chosen alternative
"""
if self._parsed is None:
try:
_, spec, params = self._alternatives[self._choice]
self._parsed, _ = _parse_build(self._contents, spec=spec, spec_params=params)
except (ValueError, TypeError) as e:
args = e.args[1:]
e.args = (e.args[0] + '\n while parsing %s' % type_name(self),) + args
raise e
return self._parsed
@property
def chosen(self):
"""
:return:
An Asn1Value object of the chosen alternative
"""
return self.parse()
@property
def native(self):
"""
The native Python datatype representation of this value
:return:
The .native value from the contained value object
"""
return self.chosen.native
def validate(self, class_, tag, contents):
"""
Ensures that the class and tag specified exist as an alternative
:param class_:
The integer class_ from the encoded value header
:param tag:
The integer tag from the encoded value header
:param contents:
A byte string of the contents of the value - used when the object
is explicitly tagged
:raises:
ValueError - when value is not a valid alternative
"""
id_ = (class_, tag)
if self.explicit is not None:
if self.explicit[-1] != id_:
raise ValueError(unwrap(
'''
%s was explicitly tagged, but the value provided does not
match the class and tag
''',
type_name(self)
))
((class_, _, tag, _, _, _), _) = _parse(contents, len(contents))
id_ = (class_, tag)
if id_ in self._id_map:
self._choice = self._id_map[id_]
return
# This means the Choice was implicitly tagged
if self.class_ is not None and self.tag is not None:
if len(self._alternatives) > 1:
raise ValueError(unwrap(
'''
%s was implicitly tagged, but more than one alternative
exists
''',
type_name(self)
))
if id_ == (self.class_, self.tag):
self._choice = 0
return
asn1 = self._format_class_tag(class_, tag)
asn1s = [self._format_class_tag(pair[0], pair[1]) for pair in self._id_map]
raise ValueError(unwrap(
'''
Value %s did not match the class and tag of any of the alternatives
in %s: %s
''',
asn1,
type_name(self),
', '.join(asn1s)
))
def _format_class_tag(self, class_, tag):
"""
:return:
A unicode string of a human-friendly representation of the class and tag
"""
return '[%s %s]' % (CLASS_NUM_TO_NAME_MAP[class_].upper(), tag)
def _copy(self, other, copy_func):
"""
Copies the contents of another Choice object to itself
:param object:
Another instance of the same class
:param copy_func:
An reference of copy.copy() or copy.deepcopy() to use when copying
lists, dicts and objects
"""
super(Choice, self)._copy(other, copy_func)
self._choice = other._choice
self._name = other._name
self._parsed = copy_func(other._parsed)
def dump(self, force=False):
"""
Encodes the value using DER
:param force:
If the encoded contents already exist, clear them and regenerate
to ensure they are in DER format instead of BER format
:return:
A byte string of the DER-encoded value
"""
# If the length is indefinite, force the re-encoding
if self._header is not None and self._header[-1:] == b'\x80':
force = True
self._contents = self.chosen.dump(force=force)
if self._header is None or force:
self._header = b''
if self.explicit is not None:
for class_, tag in self.explicit:
self._header = _dump_header(class_, 1, tag, self._header + self._contents) + self._header
return self._header + self._contents
class Concat(object):
"""
A class that contains two or more encoded child values concatentated
together. THIS IS NOT PART OF THE ASN.1 SPECIFICATION! This exists to handle
the x509.TrustedCertificate() class for OpenSSL certificates containing
extra information.
"""
# A list of the specs of the concatenated values
_child_specs = None
_children = None
@classmethod
def load(cls, encoded_data, strict=False):
"""
Loads a BER/DER-encoded byte string using the current class as the spec
:param encoded_data:
A byte string of BER or DER encoded data
:param strict:
A boolean indicating if trailing data should be forbidden - if so, a
ValueError will be raised when trailing data exists
:return:
A Concat object
"""
return cls(contents=encoded_data, strict=strict)
def __init__(self, value=None, contents=None, strict=False):
"""
:param value:
A native Python datatype to initialize the object value with
:param contents:
A byte string of the encoded contents of the value
:param strict:
A boolean indicating if trailing data should be forbidden - if so, a
ValueError will be raised when trailing data exists in contents
:raises:
ValueError - when an error occurs with one of the children
TypeError - when an error occurs with one of the children
"""
if contents is not None:
try:
contents_len = len(contents)
self._children = []
offset = 0
for spec in self._child_specs:
if offset < contents_len:
child_value, offset = _parse_build(contents, pointer=offset, spec=spec)
else:
child_value = spec()
self._children.append(child_value)
if strict and offset != contents_len:
extra_bytes = contents_len - offset
raise ValueError('Extra data - %d bytes of trailing data were provided' % extra_bytes)
except (ValueError, TypeError) as e:
args = e.args[1:]
e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args
raise e
if value is not None:
if self._children is None:
self._children = [None] * len(self._child_specs)
for index, data in enumerate(value):
self.__setitem__(index, data)
def __str__(self):
"""
Since str is different in Python 2 and 3, this calls the appropriate
method, __unicode__() or __bytes__()
:return:
A unicode string
"""
if _PY2:
return self.__bytes__()
else:
return self.__unicode__()
def __bytes__(self):
"""
A byte string of the DER-encoded contents
"""
return self.dump()
def __unicode__(self):
"""
:return:
A unicode string
"""
return repr(self)
def __repr__(self):
"""
:return:
A unicode string
"""
return '<%s %s %s>' % (type_name(self), id(self), repr(self.dump()))
def __copy__(self):
"""
Implements the copy.copy() interface
:return:
A new shallow copy of the Concat object
"""
new_obj = self.__class__()
new_obj._copy(self, copy.copy)
return new_obj
def __deepcopy__(self, memo):
"""
Implements the copy.deepcopy() interface
:param memo:
A dict for memoization
:return:
A new deep copy of the Concat object and all child objects
"""
new_obj = self.__class__()
memo[id(self)] = new_obj
new_obj._copy(self, copy.deepcopy)
return new_obj
def copy(self):
"""
Copies the object
:return:
A Concat object
"""
return copy.deepcopy(self)
def _copy(self, other, copy_func):
"""
Copies the contents of another Concat object to itself
:param object:
Another instance of the same class
:param copy_func:
An reference of copy.copy() or copy.deepcopy() to use when copying
lists, dicts and objects
"""
if self.__class__ != other.__class__:
raise TypeError(unwrap(
'''
Can not copy values from %s object to %s object
''',
type_name(other),
type_name(self)
))
self._children = copy_func(other._children)
def debug(self, nest_level=1):
"""
Show the binary data and parsed data in a tree structure
"""
prefix = ' ' * nest_level
print('%s%s Object #%s' % (prefix, type_name(self), id(self)))
print('%s Children:' % (prefix,))
for child in self._children:
child.debug(nest_level + 2)
def dump(self, force=False):
"""
Encodes the value using DER
:param force:
If the encoded contents already exist, clear them and regenerate
to ensure they are in DER format instead of BER format
:return:
A byte string of the DER-encoded value
"""
contents = b''
for child in self._children:
contents += child.dump(force=force)
return contents
@property
def contents(self):
"""
:return:
A byte string of the DER-encoded contents of the children
"""
return self.dump()
def __len__(self):
"""
:return:
Integer
"""
return len(self._children)
def __getitem__(self, key):
"""
Allows accessing children by index
:param key:
An integer of the child index
:raises:
KeyError - when an index is invalid
:return:
The Asn1Value object of the child specified
"""
if key > len(self._child_specs) - 1 or key < 0:
raise KeyError(unwrap(
'''
No child is definition for position %d of %s
''',
key,
type_name(self)
))
return self._children[key]
def __setitem__(self, key, value):
"""
Allows settings children by index
:param key:
An integer of the child index
:param value:
An Asn1Value object to set the child to
:raises:
KeyError - when an index is invalid
ValueError - when the value is not an instance of Asn1Value
"""
if key > len(self._child_specs) - 1 or key < 0:
raise KeyError(unwrap(
'''
No child is defined for position %d of %s
''',
key,
type_name(self)
))
if not isinstance(value, Asn1Value):
raise ValueError(unwrap(
'''
Value for child %s of %s is not an instance of
asn1crypto.core.Asn1Value
''',
key,
type_name(self)
))
self._children[key] = value
def __iter__(self):
"""
:return:
An iterator of child values
"""
return iter(self._children)
class Primitive(Asn1Value):
"""
Sets the class_ and method attributes for primitive, universal values
"""
class_ = 0
method = 0
def __init__(self, value=None, default=None, contents=None, **kwargs):
"""
Sets the value of the object before passing to Asn1Value.__init__()
:param value:
A native Python datatype to initialize the object value with
:param default:
The default value if no value is specified
:param contents:
A byte string of the encoded contents of the value
"""
Asn1Value.__init__(self, **kwargs)
try:
if contents is not None:
self.contents = contents
elif value is not None:
self.set(value)
elif default is not None:
self.set(default)
except (ValueError, TypeError) as e:
args = e.args[1:]
e.args = (e.args[0] + '\n while constructing %s' % type_name(self),) + args
raise e
def set(self, value):
"""
Sets the value of the object
:param value:
A byte string
"""
if not isinstance(value, byte_cls):
raise TypeError(unwrap(
'''
%s value must be a byte string, not %s
''',
type_name(self),
type_name(value)
))
self._native = value
self.contents = value
self._header = None
if self._trailer != b'':
self._trailer = b''
def dump(self, force=False):
"""
Encodes the value using DER
:param force:
If the encoded contents already exist, clear them and regenerate
to ensure they are in DER format instead of BER format
:return:
A byte string of the DER-encoded value
"""
# If the length is indefinite, force the re-encoding
if self._header is not None and self._header[-1:] == b'\x80':
force = True
if force:
native = self.native
self.contents = None
self.set(native)
return Asn1Value.dump(self)
def __ne__(self, other):
return not self == other
def __eq__(self, other):
"""
:param other:
The other Primitive to compare to
:return:
A boolean
"""
if not isinstance(other, Primitive):
return False
if self.contents != other.contents:
return False
# We compare class tag numbers since object tag numbers could be
# different due to implicit or explicit tagging
if self.__class__.tag != other.__class__.tag:
return False
if self.__class__ == other.__class__ and self.contents == other.contents:
return True
# If the objects share a common base class that is not too low-level
# then we can compare the contents
self_bases = (set(self.__class__.__bases__) | set([self.__class__])) - set([Asn1Value, Primitive, ValueMap])
other_bases = (set(other.__class__.__bases__) | set([other.__class__])) - set([Asn1Value, Primitive, ValueMap])
if self_bases | other_bases:
return self.contents == other.contents
# When tagging is going on, do the extra work of constructing new
# objects to see if the dumped representation are the same
if self.implicit or self.explicit or other.implicit or other.explicit:
return self.untag().dump() == other.untag().dump()
return self.dump() == other.dump()
class AbstractString(Constructable, Primitive):
"""
A base class for all strings that have a known encoding. In general, we do
not worry ourselves with confirming that the decoded values match a specific
set of characters, only that they are decoded into a Python unicode string
"""
# The Python encoding name to use when decoding or encoded the contents
_encoding = 'latin1'
# Instance attribute of (possibly-merged) unicode string
_unicode = None
def set(self, value):
"""
Sets the value of the string
:param value:
A unicode string
"""
if not isinstance(value, str_cls):
raise TypeError(unwrap(
'''
%s value must be a unicode string, not %s
''',
type_name(self),
type_name(value)
))
self._unicode = value
self.contents = value.encode(self._encoding)
self._header = None
if self._indefinite:
self._indefinite = False
self.method = 0
if self._trailer != b'':
self._trailer = b''
def __unicode__(self):
"""
:return:
A unicode string
"""
if self.contents is None:
return ''
if self._unicode is None:
self._unicode = self._merge_chunks().decode(self._encoding)
return self._unicode
def _copy(self, other, copy_func):
"""
Copies the contents of another AbstractString object to itself
:param object:
Another instance of the same class
:param copy_func:
An reference of copy.copy() or copy.deepcopy() to use when copying
lists, dicts and objects
"""
super(AbstractString, self)._copy(other, copy_func)
self._unicode = other._unicode
@property
def native(self):
"""
The native Python datatype representation of this value
:return:
A unicode string or None
"""
if self.contents is None:
return None
return self.__unicode__()
class Boolean(Primitive):
"""
Represents a boolean in both ASN.1 and Python
"""
tag = 1
def set(self, value):
"""
Sets the value of the object
:param value:
True, False or another value that works with bool()
"""
self._native = bool(value)
self.contents = b'\x00' if not value else b'\xff'
self._header = None
if self._trailer != b'':
self._trailer = b''
# Python 2
def __nonzero__(self):
"""
:return:
True or False
"""
return self.__bool__()
def __bool__(self):
"""
:return:
True or False
"""
return self.contents != b'\x00'
@property
def native(self):
"""
The native Python datatype representation of this value
:return:
True, False or None
"""
if self.contents is None:
return None
if self._native is None:
self._native = self.__bool__()
return self._native
class Integer(Primitive, ValueMap):
"""
Represents an integer in both ASN.1 and Python
"""
tag = 2
def set(self, value):
"""
Sets the value of the object
:param value:
An integer, or a unicode string if _map is set
:raises:
ValueError - when an invalid value is passed
"""
if isinstance(value, str_cls):
if self._map is None:
raise ValueError(unwrap(
'''
%s value is a unicode string, but no _map provided
''',
type_name(self)
))
if value not in self._reverse_map:
raise ValueError(unwrap(
'''
%s value, %s, is not present in the _map
''',
type_name(self),
value
))
value = self._reverse_map[value]
elif not isinstance(value, int_types):
raise TypeError(unwrap(
'''
%s value must be an integer or unicode string when a name_map
is provided, not %s
''',
type_name(self),
type_name(value)
))
self._native = self._map[value] if self._map and value in self._map else value
self.contents = int_to_bytes(value, signed=True)
self._header = None
if self._trailer != b'':
self._trailer = b''
def __int__(self):
"""
:return:
An integer
"""
return int_from_bytes(self.contents, signed=True)
@property
def native(self):
"""
The native Python datatype representation of this value
:return:
An integer or None
"""
if self.contents is None:
return None
if self._native is None:
self._native = self.__int__()
if self._map is not None and self._native in self._map:
self._native = self._map[self._native]
return self._native
class _IntegerBitString(object):
"""
A mixin for IntegerBitString and BitString to parse the contents as an integer.
"""
# Tuple of 1s and 0s; set through native
_unused_bits = ()
def _as_chunk(self):
"""
Parse the contents of a primitive BitString encoding as an integer value.
Allows reconstructing indefinite length values.
:raises:
ValueError - when an invalid value is passed
:return:
A list with one tuple (value, bits, unused_bits) where value is an integer
with the value of the BitString, bits is the bit count of value and
unused_bits is a tuple of 1s and 0s.
"""
if self._indefinite:
# return an empty chunk, for cases like \x23\x80\x00\x00
return []
unused_bits_len = ord(self.contents[0]) if _PY2 else self.contents[0]
value = int_from_bytes(self.contents[1:])
bits = (len(self.contents) - 1) * 8
if not unused_bits_len:
return [(value, bits, ())]
if len(self.contents) == 1:
# Disallowed by X.690 §8.6.2.3
raise ValueError('Empty bit string has {0} unused bits'.format(unused_bits_len))
if unused_bits_len > 7:
# Disallowed by X.690 §8.6.2.2
raise ValueError('Bit string has {0} unused bits'.format(unused_bits_len))
unused_bits = _int_to_bit_tuple(value & ((1 << unused_bits_len) - 1), unused_bits_len)
value >>= unused_bits_len
bits -= unused_bits_len
return [(value, bits, unused_bits)]
def _chunks_to_int(self):
"""
Combines the chunks into a single value.
:raises:
ValueError - when an invalid value is passed
:return:
A tuple (value, bits, unused_bits) where value is an integer with the
value of the BitString, bits is the bit count of value and unused_bits
is a tuple of 1s and 0s.
"""
if not self._indefinite:
# Fast path
return self._as_chunk()[0]
value = 0
total_bits = 0
unused_bits = ()
# X.690 §8.6.3 allows empty indefinite encodings
for chunk, bits, unused_bits in self._merge_chunks():
if total_bits & 7:
# Disallowed by X.690 §8.6.4
raise ValueError('Only last chunk in a bit string may have unused bits')
total_bits += bits
value = (value << bits) | chunk
return value, total_bits, unused_bits
def _copy(self, other, copy_func):
"""
Copies the contents of another _IntegerBitString object to itself
:param object:
Another instance of the same class
:param copy_func:
An reference of copy.copy() or copy.deepcopy() to use when copying
lists, dicts and objects
"""
super(_IntegerBitString, self)._copy(other, copy_func)
self._unused_bits = other._unused_bits
@property
def unused_bits(self):
"""
The unused bits of the bit string encoding.
:return:
A tuple of 1s and 0s
"""
# call native to set _unused_bits
self.native
return self._unused_bits
class BitString(_IntegerBitString, Constructable, Castable, Primitive, ValueMap):
"""
Represents a bit string from ASN.1 as a Python tuple of 1s and 0s
"""
tag = 3
_size = None
def _setup(self):
"""
Generates _reverse_map from _map
"""
ValueMap._setup(self)
cls = self.__class__
if cls._map is not None:
cls._size = max(self._map.keys()) + 1
def set(self, value):
"""
Sets the value of the object
:param value:
An integer or a tuple of integers 0 and 1
:raises:
ValueError - when an invalid value is passed
"""
if isinstance(value, set):
if self._map is None:
raise ValueError(unwrap(
'''
%s._map has not been defined
''',
type_name(self)
))
bits = [0] * self._size
self._native = value
for index in range(0, self._size):
key = self._map.get(index)
if key is None:
continue
if key in value:
bits[index] = 1
value = ''.join(map(str_cls, bits))
elif value.__class__ == tuple:
if self._map is None:
self._native = value
else:
self._native = set()
for index, bit in enumerate(value):
if bit:
name = self._map.get(index, index)
self._native.add(name)
value = ''.join(map(str_cls, value))
else:
raise TypeError(unwrap(
'''
%s value must be a tuple of ones and zeros or a set of unicode
strings, not %s
''',
type_name(self),
type_name(value)
))
if self._map is not None:
if len(value) > self._size:
raise ValueError(unwrap(
'''
%s value must be at most %s bits long, specified was %s long
''',
type_name(self),
self._size,
len(value)
))
# A NamedBitList must have trailing zero bit truncated. See
# https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf
# section 11.2,
# https://tools.ietf.org/html/rfc5280#page-134 and
# https://www.ietf.org/mail-archive/web/pkix/current/msg10443.html
value = value.rstrip('0')
size = len(value)
size_mod = size % 8
extra_bits = 0
if size_mod != 0:
extra_bits = 8 - size_mod
value += '0' * extra_bits
size_in_bytes = int(math.ceil(size / 8))
if extra_bits:
extra_bits_byte = int_to_bytes(extra_bits)
else:
extra_bits_byte = b'\x00'
if value == '':
value_bytes = b''
else:
value_bytes = int_to_bytes(int(value, 2))
if len(value_bytes) != size_in_bytes:
value_bytes = (b'\x00' * (size_in_bytes - len(value_bytes))) + value_bytes
self.contents = extra_bits_byte + value_bytes
self._unused_bits = (0,) * extra_bits
self._header = None
if self._indefinite:
self._indefinite = False
self.method = 0
if self._trailer != b'':
self._trailer = b''
def __getitem__(self, key):
"""
Retrieves a boolean version of one of the bits based on a name from the
_map
:param key:
The unicode string of one of the bit names
:raises:
ValueError - when _map is not set or the key name is invalid
:return:
A boolean if the bit is set
"""
is_int = isinstance(key, int_types)
if not is_int:
if not isinstance(self._map, dict):
raise ValueError(unwrap(
'''
%s._map has not been defined
''',
type_name(self)
))
if key not in self._reverse_map:
raise ValueError(unwrap(
'''
%s._map does not contain an entry for "%s"
''',
type_name(self),
key
))
if self._native is None:
self.native
if self._map is None:
if len(self._native) >= key + 1:
return bool(self._native[key])
return False
if is_int:
key = self._map.get(key, key)
return key in self._native
def __setitem__(self, key, value):
"""
Sets one of the bits based on a name from the _map
:param key:
The unicode string of one of the bit names
:param value:
A boolean value
:raises:
ValueError - when _map is not set or the key name is invalid
"""
is_int = isinstance(key, int_types)
if not is_int:
if self._map is None:
raise ValueError(unwrap(
'''
%s._map has not been defined
''',
type_name(self)
))
if key not in self._reverse_map:
raise ValueError(unwrap(
'''
%s._map does not contain an entry for "%s"
''',
type_name(self),
key
))
if self._native is None:
self.native
if self._map is None:
new_native = list(self._native)
max_key = len(new_native) - 1
if key > max_key:
new_native.extend([0] * (key - max_key))
new_native[key] = 1 if value else 0
self._native = tuple(new_native)
else:
if is_int:
key = self._map.get(key, key)
if value:
if key not in self._native:
self._native.add(key)
else:
if key in self._native:
self._native.remove(key)
self.set(self._native)
@property
def native(self):
"""
The native Python datatype representation of this value
:return:
If a _map is set, a set of names, or if no _map is set, a tuple of
integers 1 and 0. None if no value.
"""
# For BitString we default the value to be all zeros
if self.contents is None:
if self._map is None:
self.set(())
else:
self.set(set())
if self._native is None:
int_value, bit_count, self._unused_bits = self._chunks_to_int()
bits = _int_to_bit_tuple(int_value, bit_count)
if self._map:
self._native = set()
for index, bit in enumerate(bits):
if bit:
name = self._map.get(index, index)
self._native.add(name)
else:
self._native = bits
return self._native
class OctetBitString(Constructable, Castable, Primitive):
"""
Represents a bit string in ASN.1 as a Python byte string
"""
tag = 3
# Instance attribute of (possibly-merged) byte string
_bytes = None
# Tuple of 1s and 0s; set through native
_unused_bits = ()
def set(self, value):
"""
Sets the value of the object
:param value:
A byte string
:raises:
ValueError - when an invalid value is passed
"""
if not isinstance(value, byte_cls):
raise TypeError(unwrap(
'''
%s value must be a byte string, not %s
''',
type_name(self),
type_name(value)
))
self._bytes = value
# Set the unused bits to 0
self.contents = b'\x00' + value
self._unused_bits = ()
self._header = None
if self._indefinite:
self._indefinite = False
self.method = 0
if self._trailer != b'':
self._trailer = b''
def __bytes__(self):
"""
:return:
A byte string
"""
if self.contents is None:
return b''
if self._bytes is None:
if not self._indefinite:
self._bytes, self._unused_bits = self._as_chunk()[0]
else:
chunks = self._merge_chunks()
self._unused_bits = ()
for chunk in chunks:
if self._unused_bits:
# Disallowed by X.690 §8.6.4
raise ValueError('Only last chunk in a bit string may have unused bits')
self._unused_bits = chunk[1]
self._bytes = b''.join(chunk[0] for chunk in chunks)
return self._bytes
def _copy(self, other, copy_func):
"""
Copies the contents of another OctetBitString object to itself
:param object:
Another instance of the same class
:param copy_func:
An reference of copy.copy() or copy.deepcopy() to use when copying
lists, dicts and objects
"""
super(OctetBitString, self)._copy(other, copy_func)
self._bytes = other._bytes
self._unused_bits = other._unused_bits
def _as_chunk(self):
"""
Allows reconstructing indefinite length values
:raises:
ValueError - when an invalid value is passed
:return:
List with one tuple, consisting of a byte string and an integer (unused bits)
"""
unused_bits_len = ord(self.contents[0]) if _PY2 else self.contents[0]
if not unused_bits_len:
return [(self.contents[1:], ())]
if len(self.contents) == 1:
# Disallowed by X.690 §8.6.2.3
raise ValueError('Empty bit string has {0} unused bits'.format(unused_bits_len))
if unused_bits_len > 7:
# Disallowed by X.690 §8.6.2.2
raise ValueError('Bit string has {0} unused bits'.format(unused_bits_len))
mask = (1 << unused_bits_len) - 1
last_byte = ord(self.contents[-1]) if _PY2 else self.contents[-1]
# zero out the unused bits in the last byte.
zeroed_byte = last_byte & ~mask
value = self.contents[1:-1] + (chr(zeroed_byte) if _PY2 else bytes((zeroed_byte,)))
unused_bits = _int_to_bit_tuple(last_byte & mask, unused_bits_len)
return [(value, unused_bits)]
@property
def native(self):
"""
The native Python datatype representation of this value
:return:
A byte string or None
"""
if self.contents is None:
return None
return self.__bytes__()
@property
def unused_bits(self):
"""
The unused bits of the bit string encoding.
:return:
A tuple of 1s and 0s
"""
# call native to set _unused_bits
self.native
return self._unused_bits
class IntegerBitString(_IntegerBitString, Constructable, Castable, Primitive):
"""
Represents a bit string in ASN.1 as a Python integer
"""
tag = 3
def set(self, value):
"""
Sets the value of the object
:param value:
An integer
:raises:
ValueError - when an invalid value is passed
"""
if not isinstance(value, int_types):
raise TypeError(unwrap(
'''
%s value must be a positive integer, not %s
''',
type_name(self),
type_name(value)
))
if value < 0:
raise ValueError(unwrap(
'''
%s value must be a positive integer, not %d
''',
type_name(self),
value
))
self._native = value
# Set the unused bits to 0
self.contents = b'\x00' + int_to_bytes(value, signed=True)
self._unused_bits = ()
self._header = None
if self._indefinite:
self._indefinite = False
self.method = 0
if self._trailer != b'':
self._trailer = b''
@property
def native(self):
"""
The native Python datatype representation of this value
:return:
An integer or None
"""
if self.contents is None:
return None
if self._native is None:
self._native, __, self._unused_bits = self._chunks_to_int()
return self._native
class OctetString(Constructable, Castable, Primitive):
"""
Represents a byte string in both ASN.1 and Python
"""
tag = 4
# Instance attribute of (possibly-merged) byte string
_bytes = None
def set(self, value):
"""
Sets the value of the object
:param value:
A byte string
"""
if not isinstance(value, byte_cls):
raise TypeError(unwrap(
'''
%s value must be a byte string, not %s
''',
type_name(self),
type_name(value)
))
self._bytes = value
self.contents = value
self._header = None
if self._indefinite:
self._indefinite = False
self.method = 0
if self._trailer != b'':
self._trailer = b''
def __bytes__(self):
"""
:return:
A byte string
"""
if self.contents is None:
return b''
if self._bytes is None:
self._bytes = self._merge_chunks()
return self._bytes
def _copy(self, other, copy_func):
"""
Copies the contents of another OctetString object to itself
:param object:
Another instance of the same class
:param copy_func:
An reference of copy.copy() or copy.deepcopy() to use when copying
lists, dicts and objects
"""
super(OctetString, self)._copy(other, copy_func)
self._bytes = other._bytes
@property
def native(self):
"""
The native Python datatype representation of this value
:return:
A byte string or None
"""
if self.contents is None:
return None
return self.__bytes__()
class IntegerOctetString(Constructable, Castable, Primitive):
"""
Represents a byte string in ASN.1 as a Python integer
"""
tag = 4
# An explicit length in bytes the integer should be encoded to. This should
# generally not be used since DER defines a canonical encoding, however some
# use of this, such as when storing elliptic curve private keys, requires an
# exact number of bytes, even if the leading bytes are null.
_encoded_width = None
def set(self, value):
"""
Sets the value of the object
:param value:
An integer
:raises:
ValueError - when an invalid value is passed
"""
if not isinstance(value, int_types):
raise TypeError(unwrap(
'''
%s value must be a positive integer, not %s
''',
type_name(self),
type_name(value)
))
if value < 0:
raise ValueError(unwrap(
'''
%s value must be a positive integer, not %d
''',
type_name(self),
value
))
self._native = value
self.contents = int_to_bytes(value, signed=False, width=self._encoded_width)
self._header = None
if self._indefinite:
self._indefinite = False
self.method = 0
if self._trailer != b'':
self._trailer = b''
@property
def native(self):
"""
The native Python datatype representation of this value
:return:
An integer or None
"""
if self.contents is None:
return None
if self._native is None:
self._native = int_from_bytes(self._merge_chunks())
return self._native
def set_encoded_width(self, width):
"""
Set the explicit enoding width for the integer
:param width:
An integer byte width to encode the integer to
"""
self._encoded_width = width
# Make sure the encoded value is up-to-date with the proper width
if self.contents is not None and len(self.contents) != width:
self.set(self.native)
class ParsableOctetString(Constructable, Castable, Primitive):
tag = 4
_parsed = None
# Instance attribute of (possibly-merged) byte string
_bytes = None
def __init__(self, value=None, parsed=None, **kwargs):
"""
Allows providing a parsed object that will be serialized to get the
byte string value
:param value:
A native Python datatype to initialize the object value with
:param parsed:
If value is None and this is an Asn1Value object, this will be
set as the parsed value, and the value will be obtained by calling
.dump() on this object.
"""
set_parsed = False
if value is None and parsed is not None and isinstance(parsed, Asn1Value):
value = parsed.dump()
set_parsed = True
Primitive.__init__(self, value=value, **kwargs)
if set_parsed:
self._parsed = (parsed, parsed.__class__, None)
def set(self, value):
"""
Sets the value of the object
:param value:
A byte string
"""
if not isinstance(value, byte_cls):
raise TypeError(unwrap(
'''
%s value must be a byte string, not %s
''',
type_name(self),
type_name(value)
))
self._bytes = value
self.contents = value
self._header = None
if self._indefinite:
self._indefinite = False
self.method = 0
if self._trailer != b'':
self._trailer = b''
def parse(self, spec=None, spec_params=None):
"""
Parses the contents generically, or using a spec with optional params
:param spec:
A class derived from Asn1Value that defines what class_ and tag the
value should have, and the semantics of the encoded value. The
return value will be of this type. If omitted, the encoded value
will be decoded using the standard universal tag based on the
encoded tag number.
:param spec_params:
A dict of params to pass to the spec object
:return:
An object of the type spec, or if not present, a child of Asn1Value
"""
if self._parsed is None or self._parsed[1:3] != (spec, spec_params):
parsed_value, _ = _parse_build(self.__bytes__(), spec=spec, spec_params=spec_params)
self._parsed = (parsed_value, spec, spec_params)
return self._parsed[0]
def __bytes__(self):
"""
:return:
A byte string
"""
if self.contents is None:
return b''
if self._bytes is None:
self._bytes = self._merge_chunks()
return self._bytes
def _setable_native(self):
"""
Returns a byte string that can be passed into .set()
:return:
A python value that is valid to pass to .set()
"""
return self.__bytes__()
def _copy(self, other, copy_func):
"""
Copies the contents of another ParsableOctetString object to itself
:param object:
Another instance of the same class
:param copy_func:
An reference of copy.copy() or copy.deepcopy() to use when copying
lists, dicts and objects
"""
super(ParsableOctetString, self)._copy(other, copy_func)
self._bytes = other._bytes
self._parsed = copy_func(other._parsed)
@property
def native(self):
"""
The native Python datatype representation of this value
:return:
A byte string or None
"""
if self.contents is None:
return None
if self._parsed is not None:
return self._parsed[0].native
else:
return self.__bytes__()
@property
def parsed(self):
"""
Returns the parsed object from .parse()
:return:
The object returned by .parse()
"""
if self._parsed is None:
self.parse()
return self._parsed[0]
def dump(self, force=False):
"""
Encodes the value using DER
:param force:
If the encoded contents already exist, clear them and regenerate
to ensure they are in DER format instead of BER format
:return:
A byte string of the DER-encoded value
"""
# If the length is indefinite, force the re-encoding
if self._indefinite:
force = True
if force:
if self._parsed is not None:
native = self.parsed.dump(force=force)
else:
native = self.native
self.contents = None
self.set(native)
return Asn1Value.dump(self)
class ParsableOctetBitString(ParsableOctetString):
tag = 3
def set(self, value):
"""
Sets the value of the object
:param value:
A byte string
:raises:
ValueError - when an invalid value is passed
"""
if not isinstance(value, byte_cls):
raise TypeError(unwrap(
'''
%s value must be a byte string, not %s
''',
type_name(self),
type_name(value)
))
self._bytes = value
# Set the unused bits to 0
self.contents = b'\x00' + value
self._header = None
if self._indefinite:
self._indefinite = False
self.method = 0
if self._trailer != b'':
self._trailer = b''
def _as_chunk(self):
"""
Allows reconstructing indefinite length values
:raises:
ValueError - when an invalid value is passed
:return:
A byte string
"""
unused_bits_len = ord(self.contents[0]) if _PY2 else self.contents[0]
if unused_bits_len:
raise ValueError('ParsableOctetBitString should have no unused bits')
return self.contents[1:]
class Null(Primitive):
"""
Represents a null value in ASN.1 as None in Python
"""
tag = 5
contents = b''
def set(self, value):
"""
Sets the value of the object
:param value:
None
"""
self.contents = b''
@property
def native(self):
"""
The native Python datatype representation of this value
:return:
None
"""
return None
class ObjectIdentifier(Primitive, ValueMap):
"""
Represents an object identifier in ASN.1 as a Python unicode dotted
integer string
"""
tag = 6
# A unicode string of the dotted form of the object identifier
_dotted = None
@classmethod
def map(cls, value):
"""
Converts a dotted unicode string OID into a mapped unicode string
:param value:
A dotted unicode string OID
:raises:
ValueError - when no _map dict has been defined on the class
TypeError - when value is not a unicode string
:return:
A mapped unicode string
"""
if cls._map is None:
raise ValueError(unwrap(
'''
%s._map has not been defined
''',
type_name(cls)
))
if not isinstance(value, str_cls):
raise TypeError(unwrap(
'''
value must be a unicode string, not %s
''',
type_name(value)
))
return cls._map.get(value, value)
@classmethod
def unmap(cls, value):
"""
Converts a mapped unicode string value into a dotted unicode string OID
:param value:
A mapped unicode string OR dotted unicode string OID
:raises:
ValueError - when no _map dict has been defined on the class or the value can't be unmapped
TypeError - when value is not a unicode string
:return:
A dotted unicode string OID
"""
if cls not in _SETUP_CLASSES:
cls()._setup()
_SETUP_CLASSES[cls] = True
if cls._map is None:
raise ValueError(unwrap(
'''
%s._map has not been defined
''',
type_name(cls)
))
if not isinstance(value, str_cls):
raise TypeError(unwrap(
'''
value must be a unicode string, not %s
''',
type_name(value)
))