diff --git a/docs/source/contributing/conventions/protocol-family-grouping.rst b/docs/source/contributing/conventions/protocol-family-grouping.rst index a0645bf348..158b5563d1 100644 --- a/docs/source/contributing/conventions/protocol-family-grouping.rst +++ b/docs/source/contributing/conventions/protocol-family-grouping.rst @@ -63,8 +63,8 @@ holds the ruling. By the ``id()`` criterion, TLS 1.3**, in the ARP shape (:ref:`protocol-family-shapes`): ``id()`` stays ``('TLS',)`` and ``alias`` names the version read. Only record version ``0x0300`` is branded SSLv3, in ``name`` as well as ``alias``; every later version is TLS. -:class:`~pcapkit.protocols.application.tls.SSLv3` ``('SSLv3',)`` narrows ``id()`` and -nothing else, as :class:`~pcapkit.protocols.link.arp.InARP` does under ARP, and nothing +:class:`~pcapkit.protocols.application.sslv3.SSLv3` ``('SSLv3',)`` narrows ``id()`` and +nothing else, as :class:`~pcapkit.protocols.link.inarp.InARP` does under ARP, and nothing dispatches to it: an SSLv3 record on TCP port 443 parses as TLS with alias ``SSLv3``. :class:`~pcapkit.protocols.application.dtls.DTLS` ``('DTLS',)`` reads DTLS 1.0, 1.2 and 1.3 the same way. @@ -345,6 +345,5 @@ holds :class:`~pcapkit.protocols.link.linux_sll1.LinuxSLL1`. **The** ``schema/`` ``data/`` **modules follow the same layout**: the top-level class takes the module's name, and sub-structure classes such as :class:`~pcapkit.protocols.schema.internet.igmpv3.Query` share its module. Ruled on -:issue:`2529`, where the classes that do not match yet are pending: -:class:`tls.SSLv3 ` and +:issue:`2529`, where the class that does not match yet is pending: :class:`null.NoPayload `. diff --git a/docs/source/ext.rst b/docs/source/ext.rst index e93604174c..d4aad021cf 100644 --- a/docs/source/ext.rst +++ b/docs/source/ext.rst @@ -116,7 +116,7 @@ The protocol classes :mod:`pcapkit` ships: + + +-----------------------+-------------------------------------------------------------+ | | | :class:`pcapkit.protocols.application.tls.TLS` | + + +-----------------------+-------------------------------------------------------------+ -| | SSL Family | :class:`pcapkit.protocols.application.tls.SSLv3` | +| | SSL Family | :class:`pcapkit.protocols.application.sslv3.SSLv3` | + + +-----------------------+-------------------------------------------------------------+ | | | :class:`pcapkit.protocols.application.dtls.DTLS` | + + +-----------------------+-------------------------------------------------------------+ diff --git a/docs/source/pcapkit/protocols/application/index.rst b/docs/source/pcapkit/protocols/application/index.rst index 54fd274b2b..e74e2d09f8 100644 --- a/docs/source/pcapkit/protocols/application/index.rst +++ b/docs/source/pcapkit/protocols/application/index.rst @@ -26,6 +26,7 @@ function rather than encapsulation, see ssl sslv2 tls + sslv3 dtls .. todo:: diff --git a/docs/source/pcapkit/protocols/application/ssl.rst b/docs/source/pcapkit/protocols/application/ssl.rst index e2654da645..19cb864aeb 100644 --- a/docs/source/pcapkit/protocols/application/ssl.rst +++ b/docs/source/pcapkit/protocols/application/ssl.rst @@ -11,7 +11,7 @@ family [*]_ -- the SSL versions, Transport Layer Security and Datagram TLS: * :class:`~pcapkit.protocols.application.sslv2.SSLv2` -- SSL 2.0, whose record layer shares no wire format with the others; * :class:`~pcapkit.protocols.application.tls.TLS` -- SSLv3 and TLS 1.0 - through TLS 1.3, with :class:`~pcapkit.protocols.application.tls.SSLv3` as + through TLS 1.3, with :class:`~pcapkit.protocols.application.sslv3.SSLv3` as its thin subclass for explicit construction; * :class:`~pcapkit.protocols.application.dtls.DTLS` -- DTLS 1.0, 1.2 and 1.3. diff --git a/docs/source/pcapkit/protocols/application/sslv3.rst b/docs/source/pcapkit/protocols/application/sslv3.rst new file mode 100644 index 0000000000..2bdcff3501 --- /dev/null +++ b/docs/source/pcapkit/protocols/application/sslv3.rst @@ -0,0 +1,26 @@ +SSLv3 - Secure Sockets Layer version 3 +====================================== + +.. module:: pcapkit.protocols.application.sslv3 + +:mod:`pcapkit.protocols.application.sslv3` contains +:class:`~pcapkit.protocols.application.sslv3.SSLv3` only, +which implements an extractor for Secure Sockets Layer +version 3 (SSLv3) [*]_, whose record is the one +:mod:`pcapkit.protocols.application.tls` describes. + +Nothing dispatches to :class:`~pcapkit.protocols.application.sslv3.SSLv3`: +an SSLv3 record parses as :class:`~pcapkit.protocols.application.tls.TLS` +with alias ``SSLv3``. + +.. versionadded:: 1.6.0 + +.. autoclass:: pcapkit.protocols.application.sslv3.SSLv3 + :no-members: + :show-inheritance: + + .. automethod:: id + +.. rubric:: Footnotes + +.. [*] https://datatracker.ietf.org/doc/html/rfc6101 diff --git a/docs/source/pcapkit/protocols/application/tls.rst b/docs/source/pcapkit/protocols/application/tls.rst index 724da359e2..5054e98933 100644 --- a/docs/source/pcapkit/protocols/application/tls.rst +++ b/docs/source/pcapkit/protocols/application/tls.rst @@ -4,8 +4,7 @@ TLS/SSL - Transport Layer Security .. module:: pcapkit.protocols.application.tls :mod:`pcapkit.protocols.application.tls` contains -:class:`~pcapkit.protocols.application.tls.TLS` and -:class:`~pcapkit.protocols.application.tls.SSLv3`, which implement an +:class:`~pcapkit.protocols.application.tls.TLS` only, which implements an extractor for the TLS/SSLv3+ record-layer family [*]_ -- SSLv3 (:rfc:`6101`), TLS 1.0 (:rfc:`2246`), TLS 1.1 (:rfc:`4346`), TLS 1.2 (:rfc:`5246`) and TLS 1.3 (:rfc:`9846`, which obsoletes :rfc:`5077`, :rfc:`5246`, :rfc:`6961`, @@ -61,9 +60,9 @@ negotiated one. So in a TLS 1.3 connection, every record after the hellos carries ``0x0303`` and reads ``TLS 1.2``; and a TLS 1.2 ``ClientHello`` sent at record version ``0x0301`` reads ``TLS 1.0``. -:class:`~pcapkit.protocols.application.tls.SSLv3` is for explicit +:class:`~pcapkit.protocols.application.sslv3.SSLv3` is for explicit construction only, mirroring :class:`~pcapkit.protocols.link.inarp.InARP`: it -narrows :meth:`~pcapkit.protocols.application.tls.SSLv3.id` to ``('SSLv3',)`` +narrows :meth:`~pcapkit.protocols.application.sslv3.SSLv3.id` to ``('SSLv3',)`` and inherits :meth:`~pcapkit.protocols.application.tls.TLS.read` unchanged, so the alias still follows the wire. Dispatch never constructs one. @@ -133,12 +132,6 @@ this is routine. .. automethod:: _make_data -.. autoclass:: pcapkit.protocols.application.tls.SSLv3 - :no-members: - :show-inheritance: - - .. automethod:: id - Header Schema ------------- diff --git a/docs/source/pcapkit/protocols/index.rst b/docs/source/pcapkit/protocols/index.rst index 3de813ef97..29800a96c7 100644 --- a/docs/source/pcapkit/protocols/index.rst +++ b/docs/source/pcapkit/protocols/index.rst @@ -186,7 +186,7 @@ from given information. The class hierarchy of :mod:`pcapkit.protocols`: click SSL "application/ssl.html#pcapkit.protocols.application.ssl.SSL" click SSLv2 "application/sslv2.html#pcapkit.protocols.application.sslv2.SSLv2" click TLS "application/tls.html#pcapkit.protocols.application.tls.TLS" - click SSLv3 "application/tls.html#pcapkit.protocols.application.tls.SSLv3" + click SSLv3 "application/sslv3.html#pcapkit.protocols.application.sslv3.SSLv3" click DTLS "application/dtls.html#pcapkit.protocols.application.dtls.DTLS" click Raw "misc/raw.html#pcapkit.protocols.misc.raw.Raw" diff --git a/pcapkit/protocols/application/__init__.py b/pcapkit/protocols/application/__init__.py index 4884619597..82256caaf8 100644 --- a/pcapkit/protocols/application/__init__.py +++ b/pcapkit/protocols/application/__init__.py @@ -30,7 +30,8 @@ from pcapkit.protocols.application.rarp import RARP from pcapkit.protocols.application.drarp import DRARP from pcapkit.protocols.application.sslv2 import SSLv2 -from pcapkit.protocols.application.tls import TLS, SSLv3 +from pcapkit.protocols.application.tls import TLS +from pcapkit.protocols.application.sslv3 import SSLv3 from pcapkit.protocols.application.dtls import DTLS # Base Classes diff --git a/pcapkit/protocols/application/ssl.py b/pcapkit/protocols/application/ssl.py index d87023843d..148c89df82 100644 --- a/pcapkit/protocols/application/ssl.py +++ b/pcapkit/protocols/application/ssl.py @@ -9,7 +9,7 @@ which is the umbrella base class for the Secure Sockets Layer protocol family [*]_ -- the SSL versions, Transport Layer Security (:class:`~pcapkit.protocols.application.tls.TLS`, which also covers SSLv3 -through :class:`~pcapkit.protocols.application.tls.SSLv3`) and Datagram TLS +through :class:`~pcapkit.protocols.application.sslv3.SSLv3`) and Datagram TLS (:class:`~pcapkit.protocols.application.dtls.DTLS`). The TLS record header (:rfc:`9846#section-5.1`) is a diff --git a/pcapkit/protocols/application/sslv3.py b/pcapkit/protocols/application/sslv3.py new file mode 100644 index 0000000000..b835b2383f --- /dev/null +++ b/pcapkit/protocols/application/sslv3.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +"""SSLv3 - Secure Sockets Layer version 3 +============================================ + +.. module:: pcapkit.protocols.application.sslv3 + +:mod:`pcapkit.protocols.application.sslv3` contains +:class:`~pcapkit.protocols.application.sslv3.SSLv3` only, +which implements an extractor for Secure Sockets Layer +version 3 (SSLv3) [*]_, whose record is the one +:mod:`pcapkit.protocols.application.tls` describes. + +Nothing dispatches to :class:`~pcapkit.protocols.application.sslv3.SSLv3`: +an SSLv3 record parses as :class:`~pcapkit.protocols.application.tls.TLS` +with alias ``SSLv3``. + +.. [*] https://datatracker.ietf.org/doc/html/rfc6101 + +.. versionadded:: 1.6.0 + +""" +from typing import TYPE_CHECKING + +from pcapkit.protocols.application.tls import TLS +from pcapkit.protocols.data.application.tls import TLS as Data_TLS +from pcapkit.protocols.schema.application.tls import TLS as Schema_TLS + +if TYPE_CHECKING: + from typing_extensions import Literal + +__all__ = ['SSLv3'] + + +class SSLv3(TLS, schema=Schema_TLS, data=Data_TLS): + """This class implements Secure Sockets Layer version 3. + + Dispatch never constructs it: an SSLv3 record parses as + :class:`~pcapkit.protocols.application.tls.TLS` with + :attr:`~pcapkit.protocols.application.tls.TLS.alias` ``'SSLv3'``. Like + :class:`~pcapkit.protocols.link.inarp.InARP` to + :class:`~pcapkit.protocols.link.arp.ARP`, it narrows :meth:`id` only. + + """ + + ########################################################################## + # Methods. + ########################################################################## + + @classmethod + def id(cls) -> 'tuple[Literal["SSLv3"]]': # type: ignore[override] + """Index ID of the protocol.""" + return ('SSLv3',) diff --git a/pcapkit/protocols/application/tls.py b/pcapkit/protocols/application/tls.py index c977b9deb9..4e24cca544 100644 --- a/pcapkit/protocols/application/tls.py +++ b/pcapkit/protocols/application/tls.py @@ -5,9 +5,8 @@ .. module:: pcapkit.protocols.application.tls :mod:`pcapkit.protocols.application.tls` contains -:class:`~pcapkit.protocols.application.tls.TLS` and -:class:`~pcapkit.protocols.application.tls.SSLv3`, -which implement an extractor for the TLS/SSLv3+ record-layer family -- +:class:`~pcapkit.protocols.application.tls.TLS` only, +which implements an extractor for the TLS/SSLv3+ record-layer family -- SSLv3, TLS 1.0, TLS 1.1, TLS 1.2 (:rfc:`5246`) and TLS 1.3 (:rfc:`9846`, which obsoletes :rfc:`5077`, :rfc:`5246`, :rfc:`6961`, :rfc:`7627`, :rfc:`8422` and :rfc:`8446`). :attr:`TCP.__proto__ @@ -88,7 +87,7 @@ from pcapkit.protocols.protocol import ProtocolBase from pcapkit.protocols.schema.schema import Schema -__all__ = ['TLS', 'SSLv3'] +__all__ = ['TLS'] #: Length of the record header: content type, legacy version and length #: (:rfc:`9846#section-5.1`). @@ -136,7 +135,7 @@ def _opens_whole_record(data: 'bytes') -> 'bool': class TLS(SSL[Data_TLS, Schema_TLS], schema=Schema_TLS, data=Data_TLS): """This class implements all protocols in the TLS/SSLv3+ record-layer family. - - Secure Sockets Layer version 3 (:class:`SSLv3`) + - Secure Sockets Layer version 3 (:class:`~pcapkit.protocols.application.sslv3.SSLv3`) - Transport Layer Security 1.0 [:rfc:`2246`] - Transport Layer Security 1.1 [:rfc:`4346`] - Transport Layer Security 1.2 [:rfc:`5246`] @@ -526,23 +525,3 @@ def _import_next_record(self, proto: 'int', length: 'Optional[int]' = None, *, return self._parse_next_layer(type(self), file_, len(file_) if length is None else length, alias=proto, packet=packet, layer=self._exlayer, protocol=self._exproto, __context__=self._exctx, chained=True) - - - -class SSLv3(TLS, schema=Schema_TLS, data=Data_TLS): - """This class implements Secure Sockets Layer version 3. - - Dispatch never constructs it: an SSLv3 record parses as :class:`TLS` with - :attr:`~TLS.alias` ``'SSLv3'``. Like :class:`~pcapkit.protocols.link.inarp.InARP` - to :class:`~pcapkit.protocols.link.arp.ARP`, it narrows :meth:`id` only. - - """ - - ########################################################################## - # Methods. - ########################################################################## - - @classmethod - def id(cls) -> 'tuple[Literal["SSLv3"]]': # type: ignore[override] - """Index ID of the protocol.""" - return ('SSLv3',) diff --git a/tests/protocols/application/test_ssl_tls_dtls_unit.py b/tests/protocols/application/test_ssl_tls_dtls_unit.py index b12e5f57e2..e7e3144e92 100644 --- a/tests/protocols/application/test_ssl_tls_dtls_unit.py +++ b/tests/protocols/application/test_ssl_tls_dtls_unit.py @@ -800,7 +800,7 @@ def test_sslv3_is_constructible_directly_and_narrows_id(self) -> None: :class:`~pcapkit.protocols.application.tls.TLS` would. """ - from pcapkit.protocols.application.tls import SSLv3 + from pcapkit.protocols.application.sslv3 import SSLv3 tls_wire = b'\x17\x03\x00\x00\x05hello' parsed = SSLv3(tls_wire, len(tls_wire)) @@ -816,7 +816,7 @@ def test_sslv3_class_does_not_force_its_own_alias(self) -> None: it happened to be instantiated as. """ - from pcapkit.protocols.application.tls import SSLv3 + from pcapkit.protocols.application.sslv3 import SSLv3 tls_1_0_wire = b'\x17\x03\x01\x00\x05hello' parsed = SSLv3(tls_1_0_wire, len(tls_1_0_wire)) @@ -1168,7 +1168,8 @@ def test_exported_from_every_namespace(self) -> None: import pcapkit.protocols.application from pcapkit.protocols.application.dtls import DTLS from pcapkit.protocols.application.ssl import SSL - from pcapkit.protocols.application.tls import TLS, SSLv3 + from pcapkit.protocols.application.sslv3 import SSLv3 + from pcapkit.protocols.application.tls import TLS classes = {'SSL': SSL, 'SSLv3': SSLv3, 'TLS': TLS, 'DTLS': DTLS} for module in (pcapkit, pcapkit.all, pcapkit.protocols, pcapkit.protocols.application): diff --git a/tests/protocols/application/test_sslv3_unit.py b/tests/protocols/application/test_sslv3_unit.py new file mode 100644 index 0000000000..2c159990ff --- /dev/null +++ b/tests/protocols/application/test_sslv3_unit.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +"""Unit tests for :mod:`pcapkit.protocols.application.sslv3`. + +#2529: a module holds one protocol class, named after it, so ``SSLv3`` has a +module of its own beside :mod:`pcapkit.protocols.application.tls`. Every case +builds its own octets. + +""" +from __future__ import annotations + +import importlib +import importlib.util +import unittest + +from tests._support import reimport_once_per_class + +RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') +HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) + +#: An SSLv3 ``close_notify`` warning alert, :rfc:`6101#section-5.4.2`: content +#: type 21, version ``{3, 0}``, length 2, then level 1 and description 0. +SSLV3_ALERT = bytes.fromhex('15' '0300' '0002' '01' '00') + + +@unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') +class SSLv3ModuleTests(unittest.TestCase): + """``SSLv3`` lives in its own module and keeps TLS's behaviour.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def test_sslv3_is_defined_in_its_own_module(self) -> None: + from pcapkit.protocols.application.sslv3 import SSLv3 + from pcapkit.protocols.application.tls import TLS + + sslv3 = importlib.import_module('pcapkit.protocols.application.sslv3') + self.assertEqual(SSLv3.__module__, 'pcapkit.protocols.application.sslv3') + self.assertEqual(sslv3.__all__, ['SSLv3']) + self.assertEqual(SSLv3.__bases__, (TLS,)) + + def test_tls_module_holds_tls_alone(self) -> None: + """#2529: one protocol class per module.""" + from pcapkit.protocols.protocol import ProtocolBase + + tls = importlib.import_module('pcapkit.protocols.application.tls') + defined = [name for name, obj in vars(tls).items() + if isinstance(obj, type) and issubclass(obj, ProtocolBase) + and obj.__module__ == tls.__name__] + self.assertEqual(tls.__all__, ['TLS']) + self.assertEqual(defined, ['TLS']) + + def test_id_and_index(self) -> None: + """``SSLv3`` narrows ``id()`` to itself and inherits the rest of TLS.""" + from pcapkit.protocols.application.sslv3 import SSLv3 + from pcapkit.protocols.application.tls import TLS + + self.assertEqual(SSLv3.id(), ('SSLv3',)) + self.assertEqual(TLS.id(), ('TLS',)) + self.assertEqual([name for name in vars(SSLv3) if not name.startswith('_')], ['id']) + self.assertNotIn('__index__', vars(SSLv3)) + + def test_top_level_names_are_the_new_homes_object(self) -> None: + import pcapkit + import pcapkit.all + import pcapkit.protocols + import pcapkit.protocols.application + from pcapkit.protocols.application.sslv3 import SSLv3 + + for package in (pcapkit, pcapkit.all, pcapkit.protocols, pcapkit.protocols.application): + with self.subTest(package=package.__name__): + self.assertIs(package.SSLv3, SSLv3) + self.assertIn('SSLv3', package.__all__) + self.assertIs(pcapkit.protocols.__proto__['SSLV3'], SSLv3) + + def test_parsed_tls_keeps_its_dynamic_sslv3_alias(self) -> None: + """Dispatch binds TLS alone; legacy version ``0x0300`` reports ``SSLv3``.""" + from pcapkit.protocols.application.sslv3 import SSLv3 + from pcapkit.protocols.application.tls import TLS + + parsed = TLS(SSLV3_ALERT, len(SSLV3_ALERT)) + self.assertIs(type(parsed), TLS) + self.assertEqual(parsed.alias, 'SSLv3') + + direct = SSLv3(SSLV3_ALERT, len(SSLV3_ALERT)) + self.assertEqual(direct.alias, 'SSLv3') + self.assertEqual(direct.name, 'Secure Sockets Layer version 3') + self.assertEqual((direct.info.content_type, direct.info.version, direct.info.length), + (21, 0x0300, 2)) + self.assertEqual((direct.info.content.level, direct.info.content.description), (1, 0)) + + rebuilt = SSLv3.from_data(direct.info) + self.assertIs(type(rebuilt), SSLv3) + self.assertEqual(bytes(rebuilt), SSLV3_ALERT) + + +if __name__ == '__main__': + unittest.main()