X-Git-Url: http://www.git.cypherpunks.ru/?p=pyderasn.git;a=blobdiff_plain;f=pyderasn.py;h=4ae1eb0814182f065fa045d1c89b7a9f6c11e9ba;hp=b1f029cdd89e00ec6d99f5400e9bb79ba3f7a2f3;hb=f2ceb9912635bbb6e8999a257bcc5fdb473df01c;hpb=664822cb0fc85360a5f77d93ed54714c42557c74 diff --git a/pyderasn.py b/pyderasn.py index b1f029c..4ae1eb0 100755 --- a/pyderasn.py +++ b/pyderasn.py @@ -298,6 +298,17 @@ SetOf _____ .. 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 @@ -392,6 +403,16 @@ TagClassReprs = { 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 @@ -496,10 +517,14 @@ _hexencoder = getencoder("hex") 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") @@ -523,6 +548,16 @@ def zero_ended_encode(num): 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) @@ -531,8 +566,14 @@ def tag_encode(num, klass=TagClassUniversal, form=TagFormPrimitive): 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 @@ -547,10 +588,14 @@ def tag_decode(tag): 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) @@ -612,6 +657,9 @@ def len_decode(data): 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", @@ -649,6 +697,8 @@ class Obj(object): @property def ready(self): # pragma: no cover + """Is object ready to be encoded? + """ raise NotImplementedError() def _assert_ready(self): @@ -657,9 +707,13 @@ class Obj(object): @property def decoded(self): + """Is object decoded? + """ return self.llen > 0 def copy(self): # pragma: no cover + """Make a copy of object, safe to be mutated + """ raise NotImplementedError() @property @@ -686,6 +740,14 @@ class Obj(object): 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( @@ -888,6 +950,15 @@ def pp_console_blob(pp): 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"): @@ -921,10 +992,6 @@ class Boolean(Obj): 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) @@ -1119,10 +1186,6 @@ class Integer(Obj): 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 @@ -3275,6 +3338,86 @@ class Any(Obj): ######################################################################## class Sequence(Obj): + """``SEQUENCE`` structure type + + You have to make specification of sequence:: + + class Extension(Sequence): + __slots__ = () + 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: + >>> 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" @@ -3518,6 +3661,10 @@ class Sequence(Obj): 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" @@ -3601,6 +3748,35 @@ class Set(Sequence): 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" @@ -3841,6 +4017,10 @@ class SequenceOf(Obj): 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" @@ -3884,7 +4064,7 @@ def main(): # pragma: no cover 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())