VERSION | 2 +- doc/news.rst | 8 ++++++++ pyderasn.py | 36 +++++++++++++++++++++++++++--------- tests/test_pyderasn.py | 91 +++++++++++++++++++++++++++++++++++++++++++++++++----- diff --git a/VERSION b/VERSION index 11e557ac2f7fa212b331b212b052bff9cae420ab4df0579c844624dd2e86bce1..201baede28f0e6e68178dea6552eff49010d545c454f0a8100f412f4e5507892 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.3 +3.4 diff --git a/doc/news.rst b/doc/news.rst index 941e3bcbf2ca211e64760eaeaefa02712a09088d46b619fde24f88dd62eadf08..2e16911fab5fde197a37a0cf2722e91e557655156a1e5dbcdb10249690cdcd15 100644 --- a/doc/news.rst +++ b/doc/news.rst @@ -1,6 +1,14 @@ News ==== +.. _release3.4: + +3.4 +--- +* Strict NumericString's value sanitation +* Invalid encoding in string types will raise ``DecodeError`` exception, + instead of ``Unicode*Error`` + .. _release3.3: 3.3 diff --git a/pyderasn.py b/pyderasn.py index c5761b4bf976e01ec8349d6896ecd8590f7ccc24d56e6250bf291747ef5b780a..06bc677bfbfd7cd0705e0844797dd2feb6cb579f03f1e8949cea6a55ef1640a1 100755 --- a/pyderasn.py +++ b/pyderasn.py @@ -473,6 +473,7 @@ from collections import OrderedDict from datetime import datetime from math import ceil from os import environ +from string import digits from six import add_metaclass from six import binary_type @@ -2255,6 +2256,13 @@ default=self.default, optional=self.optional, _decoded=(offset, llen, l), ) + except DecodeError as err: + raise DecodeError( + msg=err.msg, + klass=self.__class__, + decode_path=decode_path, + offset=offset, + ) except BoundsError as err: raise DecodeError( msg=str(err), @@ -2811,7 +2819,7 @@ 'd0bfd180d0b8d0b2d0b5d18220d0bcd0b8d180' >>> PrintableString("привет мир") Traceback (most recent call last): - UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-5: ordinal not in range(128) + pyderasn.DecodeError: 'ascii' codec can't encode characters in position 0-5: ordinal not in range(128) >>> BMPString("ада", bounds=(2, 2)) Traceback (most recent call last): @@ -2867,14 +2875,17 @@ elif isinstance(value, binary_type): value_raw = value else: raise InvalidValueType((self.__class__, text_type, binary_type)) - value_raw = ( - value_decoded.encode(self.encoding) - if value_raw is None else value_raw - ) - value_decoded = ( - value_raw.decode(self.encoding) - if value_decoded is None else value_decoded - ) + try: + value_raw = ( + value_decoded.encode(self.encoding) + if value_raw is None else value_raw + ) + value_decoded = ( + value_raw.decode(self.encoding) + if value_decoded is None else value_decoded + ) + except (UnicodeEncodeError, UnicodeDecodeError) as err: + raise DecodeError(str(err)) if not self._bound_min <= len(value_decoded) <= self._bound_max: raise BoundsError( self._bound_min, @@ -2936,6 +2947,13 @@ __slots__ = () tag_default = tag_encode(18) encoding = "ascii" asn1_type_name = "NumericString" + allowable_chars = set(digits.encode("ascii")) + + def _value_sanitize(self, value): + value = super(NumericString, self)._value_sanitize(value) + if not set(value) <= self.allowable_chars: + raise DecodeError("non-numeric value") + return value class PrintableString(CommonString): diff --git a/tests/test_pyderasn.py b/tests/test_pyderasn.py index d56a9286d6c08b39d89a1d4113273801911f0ffd27dc88a6ad4face44777c923..5200558e35480ffe0a145d1d8f618d01e7a15d7e9122eee758d42a17af6dd39c 100644 --- a/tests/test_pyderasn.py +++ b/tests/test_pyderasn.py @@ -18,6 +18,7 @@ # . from datetime import datetime from string import ascii_letters +from string import digits from string import printable from string import whitespace from unittest import TestCase @@ -48,6 +49,7 @@ from six import int2byte from six import iterbytes from six import PY2 from six import text_type +from six import unichr as six_unichr from pyderasn import _pp from pyderasn import abs_decode_path @@ -2802,35 +2804,110 @@ class TestUTF8String(StringMixin, CommonMixin, TestCase): base_klass = UTF8String +class UnicodeDecodeErrorMixin(object): + @given(text( + alphabet=''.join(six_unichr(i) for i in list(range(0x0410, 0x044f + 1))), + min_size=1, + max_size=5, + )) + def test_unicode_decode_error(self, cyrillic_text): + with self.assertRaises(DecodeError): + self.base_klass(cyrillic_text) + + class TestNumericString(StringMixin, CommonMixin, TestCase): base_klass = NumericString + def text_alphabet(self): + return digits -class TestPrintableString(StringMixin, CommonMixin, TestCase): + @given(text(alphabet=ascii_letters, min_size=1, max_size=5)) + def test_non_numeric(self, cyrillic_text): + with assertRaisesRegex(self, DecodeError, "non-numeric"): + self.base_klass(cyrillic_text) + + @given( + sets(integers(min_value=0, max_value=10), min_size=2, max_size=2), + integers(min_value=0), + lists(integers()), + ) + def test_invalid_bounds_while_decoding(self, ints, offset, decode_path): + decode_path = tuple(str(i) for i in decode_path) + value, bound_min = list(sorted(ints)) + + class String(self.base_klass): + bounds = (bound_min, bound_min) + with self.assertRaises(DecodeError) as err: + String().decode( + self.base_klass(b"1" * value).encode(), + offset=offset, + decode_path=decode_path, + ) + repr(err.exception) + self.assertEqual(err.exception.offset, offset) + self.assertEqual(err.exception.decode_path, decode_path) + + +class TestPrintableString( + UnicodeDecodeErrorMixin, + StringMixin, + CommonMixin, + TestCase, +): base_klass = PrintableString -class TestTeletexString(StringMixin, CommonMixin, TestCase): +class TestTeletexString( + UnicodeDecodeErrorMixin, + StringMixin, + CommonMixin, + TestCase, +): base_klass = TeletexString -class TestVideotexString(StringMixin, CommonMixin, TestCase): +class TestVideotexString( + UnicodeDecodeErrorMixin, + StringMixin, + CommonMixin, + TestCase, +): base_klass = VideotexString -class TestIA5String(StringMixin, CommonMixin, TestCase): +class TestIA5String( + UnicodeDecodeErrorMixin, + StringMixin, + CommonMixin, + TestCase, +): base_klass = IA5String -class TestGraphicString(StringMixin, CommonMixin, TestCase): +class TestGraphicString( + UnicodeDecodeErrorMixin, + StringMixin, + CommonMixin, + TestCase, +): base_klass = GraphicString -class TestVisibleString(StringMixin, CommonMixin, TestCase): +class TestVisibleString( + UnicodeDecodeErrorMixin, + StringMixin, + CommonMixin, + TestCase, +): base_klass = VisibleString -class TestGeneralString(StringMixin, CommonMixin, TestCase): +class TestGeneralString( + UnicodeDecodeErrorMixin, + StringMixin, + CommonMixin, + TestCase, +): base_klass = GeneralString