]> Cypherpunks.ru repositories - pyderasn.git/commitdiff
Proper VisibleString, IA5String, TeletexString, T61String validation 7.6
authorSergey Matveev <stargrave@stargrave.org>
Wed, 25 Mar 2020 10:21:58 +0000 (13:21 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Wed, 25 Mar 2020 11:16:50 +0000 (14:16 +0300)
VERSION
doc/install.rst
doc/limitations.rst
doc/news.rst
pyderasn.py
tests/test_crts.py
tests/test_pyderasn.py

diff --git a/VERSION b/VERSION
index c382960e38796254910dcdfb506d19fcf1685cf4..38abeb202c0e3b33b2a5a1b9e83a42789ffc88a5 100644 (file)
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-7.5
+7.6
index 7e56b39722f932b6538433f6a91f1798a382e908..eb9c266b32d80c26eea36ecc234ae2e85223b431 100644 (file)
@@ -4,11 +4,11 @@ Install
 Preferable way is to :ref:`download <download>` tarball with the
 signature from `official website <http://pyderasn.cypherpunks.ru/>`__::
 
-    $ [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 <download>` page.
 You could use pip (**no** OpenPGP authentication is performed!) with PyPI::
 
     $ cat > requirements.txt <<EOF
-    pyderasn==7.5 --hash=sha256:TO-BE-FILLED
+    pyderasn==7.6 --hash=sha256:TO-BE-FILLED
     six==1.14.0 --hash=sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a
     EOF
     $ pip install --requirement requirements.txt
index 89b4bd4c485ae4a893a8a8834803f6ac25ee17e5..0a0b1b1dbbe3f1a25ce5329e90fa385954166e9c 100644 (file)
@@ -1,9 +1,5 @@
 Limitations
 ===========
 
-* Strings (except for :py:class:`pyderasn.NumericString` and
-  :py:class:`pyderasn.PrintableString`) are not validated
-  in any way, except just trying to be decoded in ``ascii``,
-  ``iso-8859-1``, ``utf-8/16/32`` correspondingly
 * :py:class:`pyderasn.GeneralizedTime` does not support zero year
 * No REAL, RELATIVE OID, EXTERNAL, INSTANCE OF, EMBEDDED PDV, CHARACTER STRING
index 01607814338ec392a6cccf21bd3c36fe17ad70ea..d3286605c8ff30d672ec3324be7d15f073f754a7 100644 (file)
@@ -1,6 +1,15 @@
 News
 ====
 
+.. _release7.6:
+
+7.6
+---
+* Proper strict alphabet validation of VisibleString
+* VisibleString and IA5String also have ``allowable_chars`` property
+* Fixed TeletexString, T61String use ``iso-8859-1`` encoding (instead of
+  ``ascii``), because they are 8-bit encodings
+
 .. _release7.5:
 
 7.5
index 54f2027f914df7db6d147dacde4d3daebf519a9e..bc5358a78cfa4e487efc99dd9db3f714bf0d20ea 100755 (executable)
@@ -962,6 +962,14 @@ _______________
 .. autoclass:: pyderasn.PrintableString
    :members: __init__, allow_asterisk, allow_ampersand
 
+IA5String
+_________
+.. autoclass:: pyderasn.IA5String
+
+VisibleString
+_____________
+.. autoclass:: pyderasn.VisibleString
+
 UTCTime
 _______
 .. autoclass:: pyderasn.UTCTime
@@ -1193,7 +1201,7 @@ except ImportError:  # pragma: no cover
     def colored(what, *args, **kwargs):
         return what
 
-__version__ = "7.5"
+__version__ = "7.6"
 
 __all__ = (
     "agg_octet_string",
@@ -4710,27 +4718,25 @@ class CommonString(OctetString):
        :header-rows: 1
 
        * - Class
-         - Text Encoding
+         - Text Encoding, validation
        * - :py:class:`pyderasn.UTF8String`
          - utf-8
        * - :py:class:`pyderasn.NumericString`
-         - ascii
+         - proper alphabet validation
        * - :py:class:`pyderasn.PrintableString`
-         - ascii
+         - proper alphabet validation
        * - :py:class:`pyderasn.TeletexString`
-         - ascii
+         - iso-8859-1
        * - :py:class:`pyderasn.T61String`
-         - ascii
+         - iso-8859-1
        * - :py:class:`pyderasn.VideotexString`
          - iso-8859-1
        * - :py:class:`pyderasn.IA5String`
-         - ascii
+         - proper alphabet validation
        * - :py:class:`pyderasn.GraphicString`
          - iso-8859-1
-       * - :py:class:`pyderasn.VisibleString`
-         - ascii
-       * - :py:class:`pyderasn.ISO646String`
-         - ascii
+       * - :py:class:`pyderasn.VisibleString`, :py:class:`pyderasn.ISO646String`
+         - proper alphabet validation
        * - :py:class:`pyderasn.GeneralString`
          - iso-8859-1
        * - :py:class:`pyderasn.UniversalString`
@@ -4838,6 +4844,12 @@ class AllowableCharsMixin(object):
             return self._allowable_chars
         return frozenset(six_unichr(c) for c in self._allowable_chars)
 
+    def _value_sanitize(self, value):
+        value = super(AllowableCharsMixin, self)._value_sanitize(value)
+        if not frozenset(value) <= self._allowable_chars:
+            raise DecodeError("non satisfying alphabet value")
+        return value
+
 
 class NumericString(AllowableCharsMixin, CommonString):
     """Numeric string
@@ -4854,12 +4866,6 @@ class NumericString(AllowableCharsMixin, CommonString):
     asn1_type_name = "NumericString"
     _allowable_chars = frozenset(digits.encode("ascii") + b" ")
 
-    def _value_sanitize(self, value):
-        value = super(NumericString, self)._value_sanitize(value)
-        if not frozenset(value) <= self._allowable_chars:
-            raise DecodeError("non-numeric value")
-        return value
-
 
 PrintableStringState = namedtuple(
     "PrintableStringState",
@@ -4927,12 +4933,6 @@ class PrintableString(AllowableCharsMixin, CommonString):
         """
         return self._ampersand <= self._allowable_chars
 
-    def _value_sanitize(self, value):
-        value = super(PrintableString, self)._value_sanitize(value)
-        if not frozenset(value) <= self._allowable_chars:
-            raise DecodeError("non-printable value")
-        return value
-
     def __getstate__(self):
         return PrintableStringState(
             *super(PrintableString, self).__getstate__(),
@@ -4970,7 +4970,7 @@ class PrintableString(AllowableCharsMixin, CommonString):
 class TeletexString(CommonString):
     __slots__ = ()
     tag_default = tag_encode(20)
-    encoding = "ascii"
+    encoding = "iso-8859-1"
     asn1_type_name = "TeletexString"
 
 
@@ -4986,11 +4986,27 @@ class VideotexString(CommonString):
     asn1_type_name = "VideotexString"
 
 
-class IA5String(CommonString):
+class IA5String(AllowableCharsMixin, CommonString):
+    """IA5 string
+
+    Its value is properly sanitized: it is a mix of
+
+    * http://www.itscj.ipsj.or.jp/iso-ir/006.pdf (G)
+    * http://www.itscj.ipsj.or.jp/iso-ir/001.pdf (C0)
+    * DEL character (0x7F)
+
+    It is just 7-bit ASCII.
+
+    >>> 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):
index c9517b110fb967c10b96348d97c398b786af4aa4..d209f947207947f7b4698c53cabf78e0611ab4b6 100644 (file)
@@ -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)
index d112a60c9a2ae7302f8dbf83afaeb5537a47e159..9a38fdda58bccb1837161a3237cbd5fdc9771c3d 100644 (file)
@@ -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,