From 357b330105dcecde29121a6bc203d58c6a91eb78 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Sun, 20 May 2018 19:20:21 +0300 Subject: [PATCH] BER additional tests --- doc/news.rst | 3 +- pyderasn.py | 104 +++++++------- tests/test_pyderasn.py | 311 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 338 insertions(+), 80 deletions(-) diff --git a/doc/news.rst b/doc/news.rst index 675ecf4..b66d171 100644 --- a/doc/news.rst +++ b/doc/news.rst @@ -5,8 +5,7 @@ News 3.7 --- -* Experimental BER decoding support (not extensively tested, but parses - ``gpgsm`` CMS output) +* BER decoding support * BitString's ''H notation support * ``termcolor`` package is included in the tarball diff --git a/pyderasn.py b/pyderasn.py index 50479c6..391ff9e 100755 --- a/pyderasn.py +++ b/pyderasn.py @@ -369,10 +369,6 @@ useful for SEQUENCE/SET OF-s. BER encoding ------------ -.. warning:: - - Currently BER support is not extensively tested. - By default PyDERASN accepts only DER encoded data. It always encodes to DER. But you can optionally enable BER decoding with setting ``bered`` :ref:`context ` argument to True. Indefinite lengths and @@ -2009,9 +2005,7 @@ class BitString(Obj): len(value) * 4, hexdec(value + ("" if len(value) % 2 == 0 else "0")), ) - else: - raise InvalidValueType((self.__class__, string_types, binary_type)) - elif isinstance(value, binary_type): + if isinstance(value, binary_type): return (len(value) * 8, value) else: raise InvalidValueType((self.__class__, string_types, binary_type)) @@ -2223,7 +2217,7 @@ class BitString(Obj): decode_path=decode_path, offset=offset, ) - if l > 0 and l > len(v): + if l > len(v): raise NotEnoughData( "encoded length is longer than data", klass=self.__class__, @@ -2251,7 +2245,7 @@ class BitString(Obj): raise DecodeError( "chunk out of bounds", klass=self.__class__, - decode_path=len(chunks) - 1, + decode_path=decode_path + (str(len(chunks) - 1),), offset=chunks[-1].offset, ) sub_decode_path = decode_path + (str(len(chunks)),) @@ -2286,7 +2280,7 @@ class BitString(Obj): for chunk_i, chunk in enumerate(chunks[:-1]): if chunk.bit_len % 8 != 0: raise DecodeError( - "BitString chunk is not multiple of 8 bit", + "BitString chunk is not multiple of 8 bits", klass=self.__class__, decode_path=decode_path + (str(chunk_i),), offset=chunk.offset, @@ -2591,20 +2585,13 @@ class OctetString(Obj): decode_path=decode_path, offset=offset, ) - if l > 0 and l > len(v): + if l > len(v): raise NotEnoughData( "encoded length is longer than data", klass=self.__class__, decode_path=decode_path, offset=offset, ) - if not lenindef and l == 0: - raise NotEnoughData( - "zero length", - klass=self.__class__, - decode_path=decode_path, - offset=offset, - ) chunks = [] sub_offset = offset + tlen + llen vlen = 0 @@ -2619,7 +2606,7 @@ class OctetString(Obj): raise DecodeError( "chunk out of bounds", klass=self.__class__, - decode_path=len(chunks) - 1, + decode_path=decode_path + (str(len(chunks) - 1),), offset=chunks[-1].offset, ) sub_decode_path = decode_path + (str(len(chunks)),) @@ -2642,13 +2629,6 @@ class OctetString(Obj): sub_offset += chunk.tlvlen vlen += chunk.tlvlen v = v_tail - if len(chunks) == 0: - raise DecodeError( - "no chunks", - klass=self.__class__, - decode_path=decode_path, - offset=offset, - ) try: obj = self.__class__( value=b"".join(bytes(chunk) for chunk in chunks), @@ -4036,29 +4016,27 @@ class Any(Obj): llen, vlen, v = 1, 0, lv[1:] sub_offset = offset + tlen + llen chunk_i = 0 - while True: - if v[:EOC_LEN].tobytes() == EOC: - tlvlen = tlen + llen + vlen + EOC_LEN - obj = self.__class__( - value=tlv[:tlvlen].tobytes(), - expl=self._expl, - optional=self.optional, - _decoded=(offset, 0, tlvlen), - ) - obj.lenindef = True - obj.tag = t - return obj, v[EOC_LEN:] - else: - chunk, v = Any().decode( - v, - offset=sub_offset, - decode_path=decode_path + (str(chunk_i),), - leavemm=True, - ctx=ctx, - ) - vlen += chunk.tlvlen - sub_offset += chunk.tlvlen - chunk_i += 1 + while v[:EOC_LEN].tobytes() != EOC: + chunk, v = Any().decode( + v, + offset=sub_offset, + decode_path=decode_path + (str(chunk_i),), + leavemm=True, + ctx=ctx, + ) + vlen += chunk.tlvlen + sub_offset += chunk.tlvlen + chunk_i += 1 + tlvlen = tlen + llen + vlen + EOC_LEN + obj = self.__class__( + value=tlv[:tlvlen].tobytes(), + expl=self._expl, + optional=self.optional, + _decoded=(offset, 0, tlvlen), + ) + obj.lenindef = True + obj.tag = t + return obj, v[EOC_LEN:] except DecodeError as err: raise err.__class__( msg=err.msg, @@ -4712,6 +4690,16 @@ class Set(Sequence): _decoded=(offset, llen, vlen + (EOC_LEN if lenindef else 0)), ) obj._value = values + if lenindef: + if v[:EOC_LEN].tobytes() != EOC: + raise DecodeError( + "no EOC", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + tail = v[EOC_LEN:] + obj.lenindef = True if not obj.ready: raise DecodeError( "not all values are ready", @@ -4719,8 +4707,7 @@ class Set(Sequence): decode_path=decode_path, offset=offset, ) - obj.lenindef = lenindef - return obj, (v[EOC_LEN:] if lenindef else tail) + return obj, tail class SequenceOf(Obj): @@ -4977,10 +4964,19 @@ class SequenceOf(Obj): expl=self._expl, default=self.default, optional=self.optional, - _decoded=(offset, llen, vlen), + _decoded=(offset, llen, vlen + (EOC_LEN if lenindef else 0)), ) - obj.lenindef = lenindef - return obj, (v[EOC_LEN:] if lenindef else tail) + if lenindef: + if v[:EOC_LEN].tobytes() != EOC: + raise DecodeError( + "no EOC", + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) + obj.lenindef = True + tail = v[EOC_LEN:] + return obj, tail def __repr__(self): return "%s[%s]" % ( diff --git a/tests/test_pyderasn.py b/tests/test_pyderasn.py index ef5f79e..c374415 100644 --- a/tests/test_pyderasn.py +++ b/tests/test_pyderasn.py @@ -608,6 +608,21 @@ class TestBoolean(CommonMixin, TestCase): self.assertTrue(obj.bered) self.assertFalse(obj.lenindef) + @given( + integers(min_value=1).map(tag_ctxc), + binary().filter(lambda x: not x.startswith(EOC)), + ) + def test_ber_expl_no_eoc(self, expl, junk): + encoded = expl + b"\x80" + Boolean(False).encode() + with assertRaisesRegex(self, DecodeError, "no EOC"): + Boolean(expl=expl).decode(encoded + junk, ctx={"bered": True}) + obj, tail = Boolean(expl=expl).decode( + encoded + EOC + junk, + ctx={"bered": True}, + ) + self.assertTrue(obj.expl_lenindef) + self.assertSequenceEqual(tail, junk) + @given( integers(min_value=1).map(tag_ctxc), lists( @@ -1456,8 +1471,9 @@ class TestBitString(CommonMixin, TestCase): max_size=3, ), lists(booleans(), min_size=1), + binary(), ) - def test_constructed(self, impl, chunk_inputs, chunk_last_bits): + def test_constructed(self, impl, chunk_inputs, chunk_last_bits, junk): def chunk_constructed(contents): return ( tag_encode(form=TagFormConstructed, num=3) + @@ -1503,15 +1519,132 @@ class TestBitString(CommonMixin, TestCase): (False, encoded_definite), ): obj, tail = BitString(impl=tag_encode(impl)).decode( - encoded, ctx={"bered": True} + encoded + junk, + ctx={"bered": True}, ) - self.assertSequenceEqual(tail, b"") + self.assertSequenceEqual(tail, junk) self.assertEqual(obj.bit_len, bit_len_expected) self.assertSequenceEqual(bytes(obj), payload_expected) self.assertTrue(obj.bered) self.assertEqual(obj.lenindef, lenindef_expected) self.assertEqual(len(encoded), obj.tlvlen) + @given( + integers(min_value=0), + decode_path_strat, + ) + def test_ber_definite_too_short(self, offset, decode_path): + with assertRaisesRegex(self, DecodeError, "longer than data") as err: + BitString().decode( + tag_encode(3, form=TagFormConstructed) + len_encode(1), + offset=offset, + decode_path=decode_path, + ctx={"bered": True}, + ) + self.assertEqual(err.exception.decode_path, decode_path) + self.assertEqual(err.exception.offset, offset) + + @given( + integers(min_value=0), + decode_path_strat, + ) + def test_ber_definite_no_data(self, offset, decode_path): + with assertRaisesRegex(self, DecodeError, "zero length") as err: + BitString().decode( + tag_encode(3, form=TagFormConstructed) + len_encode(0), + offset=offset, + decode_path=decode_path, + ctx={"bered": True}, + ) + self.assertEqual(err.exception.decode_path, decode_path) + self.assertEqual(err.exception.offset, offset) + + @given( + integers(min_value=0), + decode_path_strat, + integers(min_value=1, max_value=3), + ) + def test_ber_indefinite_no_eoc(self, offset, decode_path, chunks): + bs = BitString(b"data").encode() + with self.assertRaises(NotEnoughData) as err: + BitString().decode( + tag_encode(3, form=TagFormConstructed) + b"\x80" + chunks * bs, + offset=offset, + decode_path=decode_path, + ctx={"bered": True}, + ) + self.assertEqual(err.exception.decode_path, decode_path + (str(chunks),)) + self.assertEqual(err.exception.offset, offset + 1 + 1 + chunks * len(bs)) + + @given( + integers(min_value=0), + decode_path_strat, + integers(min_value=1, max_value=3), + ) + def test_ber_definite_chunk_out_of_bounds(self, offset, decode_path, chunks): + bs = BitString(b"data").encode() + bs_longer = BitString(b"data-longer").encode() + with assertRaisesRegex(self, DecodeError, "chunk out of bounds") as err: + BitString().decode( + ( + tag_encode(3, form=TagFormConstructed) + + len_encode((chunks + 1) * len(bs)) + + chunks * bs + + bs_longer + ), + offset=offset, + decode_path=decode_path, + ctx={"bered": True}, + ) + self.assertEqual(err.exception.decode_path, decode_path + (str(chunks),)) + self.assertEqual(err.exception.offset, offset + 1 + 1 + chunks * len(bs)) + + @given( + integers(min_value=0), + decode_path_strat, + ) + def test_ber_indefinite_no_chunks(self, offset, decode_path): + with assertRaisesRegex(self, DecodeError, "no chunks") as err: + BitString().decode( + tag_encode(3, form=TagFormConstructed) + b"\x80" + EOC, + offset=offset, + decode_path=decode_path, + ctx={"bered": True}, + ) + self.assertEqual(err.exception.decode_path, decode_path) + self.assertEqual(err.exception.offset, offset) + + @given(data_strategy()) + def test_ber_indefinite_not_multiple(self, d): + bs_short = BitString("'A'H").encode() + bs_full = BitString("'AA'H").encode() + chunks = [bs_full for _ in range(d.draw(integers(min_value=0, max_value=3)))] + chunks.append(bs_short) + d.draw(permutations(chunks)) + chunks.append(bs_short) + offset = d.draw(integers(min_value=0)) + decode_path = d.draw(decode_path_strat) + with assertRaisesRegex(self, DecodeError, "multiple of 8 bits") as err: + BitString().decode( + ( + tag_encode(3, form=TagFormConstructed) + + b"\x80" + + b"".join(chunks) + + EOC + ), + offset=offset, + decode_path=decode_path, + ctx={"bered": True}, + ) + self.assertEqual( + err.exception.decode_path, + decode_path + (str(chunks.index(bs_short)),), + ) + self.assertEqual( + err.exception.offset, + offset + 1 + 1 + chunks.index(bs_short) * len(bs_full), + ) + def test_x690_vector(self): vector = BitString("'0A3B5F291CD'H") obj, tail = BitString().decode(hexdec("0307040A3B5F291CD0")) @@ -1848,8 +1981,9 @@ class TestOctetString(CommonMixin, TestCase): min_size=1, max_size=3, ), + binary(), ) - def test_constructed(self, impl, chunk_inputs): + def test_constructed(self, impl, chunk_inputs, junk): def chunk_constructed(contents): return ( tag_encode(form=TagFormConstructed, num=4) + @@ -1885,14 +2019,70 @@ class TestOctetString(CommonMixin, TestCase): (False, encoded_definite), ): obj, tail = OctetString(impl=tag_encode(impl)).decode( - encoded, ctx={"bered": True} + encoded + junk, + ctx={"bered": True}, ) - self.assertSequenceEqual(tail, b"") + self.assertSequenceEqual(tail, junk) self.assertSequenceEqual(bytes(obj), payload_expected) self.assertTrue(obj.bered) self.assertEqual(obj.lenindef, lenindef_expected) self.assertEqual(len(encoded), obj.tlvlen) + @given( + integers(min_value=0), + decode_path_strat, + ) + def test_ber_definite_too_short(self, offset, decode_path): + with assertRaisesRegex(self, DecodeError, "longer than data") as err: + OctetString().decode( + tag_encode(4, form=TagFormConstructed) + len_encode(1), + offset=offset, + decode_path=decode_path, + ctx={"bered": True}, + ) + self.assertEqual(err.exception.decode_path, decode_path) + self.assertEqual(err.exception.offset, offset) + + @given( + integers(min_value=0), + decode_path_strat, + integers(min_value=1, max_value=3), + ) + def test_ber_indefinite_no_eoc(self, offset, decode_path, chunks): + bs = OctetString(b"data").encode() + with self.assertRaises(NotEnoughData) as err: + OctetString().decode( + tag_encode(4, form=TagFormConstructed) + b"\x80" + chunks * bs, + offset=offset, + decode_path=decode_path, + ctx={"bered": True}, + ) + self.assertEqual(err.exception.decode_path, decode_path + (str(chunks),)) + self.assertEqual(err.exception.offset, offset + 1 + 1 + chunks * len(bs)) + + @given( + integers(min_value=0), + decode_path_strat, + integers(min_value=1, max_value=3), + ) + def test_ber_definite_chunk_out_of_bounds(self, offset, decode_path, chunks): + bs = OctetString(b"data").encode() + bs_longer = OctetString(b"data-longer").encode() + with assertRaisesRegex(self, DecodeError, "chunk out of bounds") as err: + OctetString().decode( + ( + tag_encode(4, form=TagFormConstructed) + + len_encode((chunks + 1) * len(bs)) + + chunks * bs + + bs_longer + ), + offset=offset, + decode_path=decode_path, + ctx={"bered": True}, + ) + self.assertEqual(err.exception.decode_path, decode_path + (str(chunks),)) + self.assertEqual(err.exception.offset, offset + 1 + 1 + chunks * len(bs)) + @composite def null_values_strategy(draw, do_expl=False): @@ -3699,6 +3889,40 @@ class TestAny(CommonMixin, TestCase): self.assertEqual(obj_decoded.llen, 0) self.assertEqual(obj_decoded.vlen, len(value)) + @given( + integers(min_value=1).map(tag_ctxc), + integers(min_value=0, max_value=3), + integers(min_value=0), + decode_path_strat, + binary(), + ) + def test_indefinite(self, expl, chunks, offset, decode_path, junk): + chunk = Boolean(False, expl=expl).encode() + encoded = ( + OctetString.tag_default + + b"\x80" + + b"".join([chunk] * chunks) + + EOC + ) + obj, tail = Any().decode( + encoded + junk, + offset=offset, + decode_path=decode_path, + ctx={"bered": True}, + ) + self.assertSequenceEqual(tail, junk) + self.assertEqual(obj.offset, offset) + self.assertEqual(obj.tlvlen, len(encoded)) + with self.assertRaises(NotEnoughData) as err: + Any().decode( + encoded[:-1], + offset=offset, + decode_path=decode_path, + ctx={"bered": True}, + ) + self.assertEqual(err.exception.offset, offset + 1 + 1 + len(chunk) * chunks) + self.assertEqual(err.exception.decode_path, decode_path + (str(chunks),)) + @composite def choice_values_strategy(draw, value_required=False, schema=None, do_expl=False): @@ -4419,26 +4643,49 @@ class SeqMixing(object): self._assert_expects(seq, expects) repr(seq) pprint(seq) + self.assertTrue(seq.ready) seq_encoded = seq.encode() seq_decoded, tail = seq.decode(seq_encoded + tail_junk) - self.assertEqual(tail, tail_junk) - self.assertTrue(seq.ready) - self._assert_expects(seq_decoded, expects) - self.assertEqual(seq, seq_decoded) - self.assertEqual(seq_decoded.encode(), seq_encoded) - for expect in expects: - if not expect["presented"]: - self.assertNotIn(expect["name"], seq_decoded) - continue - self.assertIn(expect["name"], seq_decoded) - obj = seq_decoded[expect["name"]] - self.assertTrue(obj.decoded) - offset = obj.expl_offset if obj.expled else obj.offset - tlvlen = obj.expl_tlvlen if obj.expled else obj.tlvlen - self.assertSequenceEqual( - seq_encoded[offset:offset + tlvlen], - obj.encode(), - ) + self.assertFalse(seq_decoded.lenindef) + + t, _, lv = tag_strip(seq_encoded) + _, _, v = len_decode(lv) + seq_encoded_lenindef = t + b"\x80" + v + EOC + seq_decoded_lenindef, tail_lenindef = seq.decode( + seq_encoded_lenindef + tail_junk, + ctx={"bered": True}, + ) + self.assertTrue(seq_decoded_lenindef.lenindef) + with self.assertRaises(DecodeError): + seq.decode(seq_encoded_lenindef[:-1], ctx={"bered": True}) + with self.assertRaises(DecodeError): + seq.decode(seq_encoded_lenindef[:-2], ctx={"bered": True}) + repr(seq_decoded_lenindef) + pprint(seq_decoded_lenindef) + self.assertTrue(seq_decoded_lenindef.ready) + + for decoded, decoded_tail, encoded in ( + (seq_decoded, tail, seq_encoded), + (seq_decoded_lenindef, tail_lenindef, seq_encoded_lenindef), + ): + self.assertEqual(decoded_tail, tail_junk) + self._assert_expects(decoded, expects) + self.assertEqual(seq, decoded) + self.assertEqual(decoded.encode(), seq_encoded) + self.assertEqual(decoded.tlvlen, len(encoded)) + for expect in expects: + if not expect["presented"]: + self.assertNotIn(expect["name"], decoded) + continue + self.assertIn(expect["name"], decoded) + obj = decoded[expect["name"]] + self.assertTrue(obj.decoded) + offset = obj.expl_offset if obj.expled else obj.offset + tlvlen = obj.expl_tlvlen if obj.expled else obj.tlvlen + self.assertSequenceEqual( + seq_encoded[offset:offset + tlvlen], + obj.encode(), + ) @settings(max_examples=LONG_TEST_MAX_EXAMPLES) @given(data_strategy()) @@ -5011,6 +5258,22 @@ class SeqOfMixing(object): ], ) + t, _, lv = tag_strip(obj_encoded) + _, _, v = len_decode(lv) + obj_encoded_lenindef = t + b"\x80" + v + EOC + obj_decoded_lenindef, tail_lenindef = obj.decode( + obj_encoded_lenindef + tail_junk, + ctx={"bered": True}, + ) + self.assertTrue(obj_decoded_lenindef.lenindef) + repr(obj_decoded_lenindef) + pprint(obj_decoded_lenindef) + self.assertEqual(obj_decoded_lenindef.tlvlen, len(obj_encoded_lenindef)) + with self.assertRaises(DecodeError): + obj.decode(obj_encoded_lenindef[:-1], ctx={"bered": True}) + with self.assertRaises(DecodeError): + obj.decode(obj_encoded_lenindef[:-2], ctx={"bered": True}) + class TestSequenceOf(SeqOfMixing, CommonMixin, TestCase): class SeqOf(SequenceOf): -- 2.44.0