From 1285a47fd22a99a87354496ade8b392ff874d8b8 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Wed, 25 Mar 2020 13:21:58 +0300 Subject: [PATCH] Proper VisibleString, IA5String, TeletexString, T61String validation --- VERSION | 2 +- doc/install.rst | 12 +++--- doc/limitations.rst | 4 -- doc/news.rst | 9 +++++ pyderasn.py | 91 +++++++++++++++++++++++++++--------------- tests/test_crts.py | 2 +- tests/test_pyderasn.py | 57 +++++++++++++++++++++++--- 7 files changed, 127 insertions(+), 50 deletions(-) diff --git a/VERSION b/VERSION index c382960..38abeb2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.5 +7.6 diff --git a/doc/install.rst b/doc/install.rst index 7e56b39..eb9c266 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -4,11 +4,11 @@ Install Preferable way is to :ref:`download ` tarball with the signature from `official website `__:: - $ [fetch|wget] http://pyderasn.cypherpunks.ru/pyderasn-7.5.tar.xz - $ [fetch|wget] http://pyderasn.cypherpunks.ru/pyderasn-7.5.tar.xz.sig - $ gpg --verify pyderasn-7.5.tar.xz.sig pyderasn-7.5.tar.xz - $ xz --decompress --stdout pyderasn-7.5.tar.xz | tar xf - - $ cd pyderasn-7.5 + $ [fetch|wget] http://pyderasn.cypherpunks.ru/pyderasn-7.6.tar.xz + $ [fetch|wget] http://pyderasn.cypherpunks.ru/pyderasn-7.6.tar.xz.sig + $ gpg --verify pyderasn-7.6.tar.xz.sig pyderasn-7.6.tar.xz + $ xz --decompress --stdout pyderasn-7.6.tar.xz | tar xf - + $ cd pyderasn-7.6 $ python setup.py install # or copy pyderasn.py (+six.py, possibly termcolor.py) to your PYTHONPATH @@ -21,7 +21,7 @@ You can also find it mirrored on :ref:`download ` page. You could use pip (**no** OpenPGP authentication is performed!) with PyPI:: $ cat > requirements.txt <>> IA5String().allowable_chars + frozenset(["NUL", ... "DEL"]) + """ __slots__ = () tag_default = tag_encode(22) encoding = "ascii" asn1_type_name = "IA5" + _allowable_chars = frozenset(b"".join( + six_unichr(c).encode("ascii") for c in six_xrange(128) + )) LEN_YYMMDDHHMMSSZ = len("YYMMDDHHMMSSZ") @@ -5001,11 +5017,27 @@ LEN_YYYYMMDDHHMMSSZ = len("YYYYMMDDHHMMSSZ") LEN_LEN_YYYYMMDDHHMMSSZ = len_encode(LEN_YYYYMMDDHHMMSSZ) -class VisibleString(CommonString): +class VisibleString(AllowableCharsMixin, CommonString): + """Visible string + + Its value is properly sanitized. ASCII subset from space to tilde is + allowed: http://www.itscj.ipsj.or.jp/iso-ir/006.pdf + + >>> VisibleString().allowable_chars + frozenset([" ", ... "~"]) + """ __slots__ = () tag_default = tag_encode(26) encoding = "ascii" asn1_type_name = "VisibleString" + _allowable_chars = frozenset(b"".join( + six_unichr(c).encode("ascii") for c in six_xrange(ord(" "), ord("~") + 1) + )) + + +class ISO646String(VisibleString): + __slots__ = () + asn1_type_name = "ISO646String" UTCTimeState = namedtuple( @@ -5425,11 +5457,6 @@ class GraphicString(CommonString): asn1_type_name = "GraphicString" -class ISO646String(VisibleString): - __slots__ = () - asn1_type_name = "ISO646String" - - class GeneralString(CommonString): __slots__ = () tag_default = tag_encode(27) @@ -7415,7 +7442,7 @@ def ascii_visualize(ba): 92 2b 39 20 65 91 e6 8e 95 93 1a 58 df 02 78 ea |.+9 e......X..x.| ^^^^^^^^^^^^^^^^ """ - return "".join((chr(b) if 0x20 <= b <= 0x7E else ".") for b in ba) + return "".join((six_unichr(b) if 0x20 <= b <= 0x7E else ".") for b in ba) def hexdump(raw): diff --git a/tests/test_crts.py b/tests/test_crts.py index c9517b1..d209f94 100644 --- a/tests/test_crts.py +++ b/tests/test_crts.py @@ -430,5 +430,5 @@ class TestGoPayPalVector(TestCase): "07ba44cce54a2d723f9847f626dc054605076321ab469b9c78d5545b3d0c1ec86", "48cb55023826fdbb8221c439607a8bb", ))) - with assertRaisesRegex(self, DecodeError, "non-printable"): + with assertRaisesRegex(self, DecodeError, "alphabet value"): crt = Certificate().decod(raw) diff --git a/tests/test_pyderasn.py b/tests/test_pyderasn.py index d112a60..9a38fdd 100644 --- a/tests/test_pyderasn.py +++ b/tests/test_pyderasn.py @@ -3462,9 +3462,7 @@ class StringMixin(object): repr(err.exception) def text_alphabet(self): - if self.base_klass.encoding in ("ascii", "iso-8859-1"): - return printable + whitespace - return None + return "".join(six_unichr(c) for c in six_xrange(256)) @given(booleans()) def test_optional(self, optional): @@ -3828,7 +3826,7 @@ class TestNumericString(StringMixin, CommonMixin, TestCase): @given(text(alphabet=ascii_letters, min_size=1, max_size=5)) def test_non_numeric(self, non_numeric_text): - with assertRaisesRegex(self, DecodeError, "non-numeric"): + with assertRaisesRegex(self, DecodeError, "alphabet value"): self.base_klass(non_numeric_text) @given( @@ -3878,7 +3876,7 @@ class TestPrintableString( @given(text(alphabet=sorted(set(whitespace) - set(" ")), min_size=1, max_size=5)) def test_non_printable(self, non_printable_text): - with assertRaisesRegex(self, DecodeError, "non-printable"): + with assertRaisesRegex(self, DecodeError, "alphabet value"): self.base_klass(non_printable_text) @given( @@ -3912,7 +3910,7 @@ class TestPrintableString( for prop in kwargs.keys(): self.assertFalse(getattr(obj, prop)) s += c - with assertRaisesRegex(self, DecodeError, "non-printable"): + with assertRaisesRegex(self, DecodeError, "alphabet value"): self.base_klass(s) self.base_klass(s, **kwargs) klass = self.base_klass(**kwargs) @@ -3951,6 +3949,18 @@ class TestIA5String( ): base_klass = IA5String + def text_alphabet(self): + return "".join(six_unichr(c) for c in six_xrange(128)) + + @given(integers(min_value=128, max_value=255)) + def test_alphabet_bad(self, code): + with self.assertRaises(DecodeError): + self.base_klass().decod( + self.base_klass.tag_default + + len_encode(1) + + bytes(bytearray([code])), + ) + class TestGraphicString( UnicodeDecodeErrorMixin, @@ -3969,6 +3979,9 @@ class TestVisibleString( ): base_klass = VisibleString + def text_alphabet(self): + return " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" + def test_x690_vector(self): obj, tail = VisibleString().decode(hexdec("1A054A6F6E6573")) self.assertSequenceEqual(tail, b"") @@ -4005,6 +4018,38 @@ class TestVisibleString( self.assertTrue(obj.lenindef) self.assertTrue(obj.bered) + @given(one_of(( + integers(min_value=0, max_value=ord(" ") - 1), + integers(min_value=ord("~") + 1, max_value=255), + ))) + def test_alphabet_bad(self, code): + with self.assertRaises(DecodeError): + self.base_klass().decod( + self.base_klass.tag_default + + len_encode(1) + + bytes(bytearray([code])), + ) + + @given( + sets(integers(min_value=0, max_value=10), min_size=2, max_size=2), + integers(min_value=0), + decode_path_strat, + ) + def test_invalid_bounds_while_decoding(self, ints, offset, 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 TestGeneralString( UnicodeDecodeErrorMixin, -- 2.44.0