From 7def2d117bca7dff72e5438c6231d58cf08ecbf9 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Mon, 28 Sep 2026 19:48:11 -0400 Subject: [PATCH] refactor(vendor,pcapng): generate TLSKeyLabel from the RFC 9850 IANA registry - Add pcapkit/vendor/pcapng/tls_key_label.py, a new crawler fetching the IANA "TLS SSLKEYLOGFILE Labels" CSV (RFC 9850 section 4.2) and generating pcapkit/const/pcapng/tls_key_label.py, matching the shape of its const/pcapng siblings (EnumRegistry + StrEnum, no _missing_, so lookup stays closed and raises exactly as it did before this move). The RFC citation is parsed from each row rather than assumed, and raises loudly on a reference it does not recognise instead of guessing one. - RSA has no row in the registry (NSS-only, removed in NSS 3.34) and is hand-carried by the crawler with its #883 comment preserved verbatim; the other 8 "*_SECRET*" members keep their `# nosec B105` suppression. - pcapkit/protocols/misc/pcapng.py now imports TLSKeyLabel as Enum_TLSKeyLabel, matching the alias every one of its seven pcapkit.const.pcapng siblings already carries in this file, and re-exports it under the original bare name (`TLSKeyLabel = Enum_TLSKeyLabel`) so `from pcapkit.protocols.misc.pcapng import TLSKeyLabel` keeps resolving to the identical object; the three other internal importers are left importing through that re-export, to avoid splitting their existing combined import with WireGuardKeyLabel, which stays put and hand-written (the alias convention is about imports of const enums, not local definitions). Updated the one docstring elsewhere that named TLSKeyLabel's old StrEnum base, which this move changes from compat.StrEnum to aenum.StrEnum. - Register the new crawler/const module in the vendor/const __init__.py index files and docs, and pin the const-enum sweep tests' updated counts in both files that census pcapkit/const's population. - Extend test_pcapng_unit.py to check the re-export identity, pin every member's value, and confirm unknown/lower-case labels still raise. Build/test: ran the crawler twice (byte-identical sha256), plus tests/protocols/misc/test_pcapng_unit.py (94/94) and the affected tests/const + tests/vendor + tests/project sweeps, all green. --- docs/source/pcapkit/const/pcapng.rst | 16 ++ docs/source/pcapkit/protocols/misc/pcapng.rst | 10 +- docs/source/pcapkit/vendor/pcapng.rst | 15 ++ pcapkit/const/__init__.py | 2 +- pcapkit/const/pcapng/__init__.py | 6 +- pcapkit/const/pcapng/tls_key_label.py | 59 +++++++ pcapkit/foundation/reassembly/data/data.py | 16 +- pcapkit/protocols/misc/pcapng.py | 44 ++--- pcapkit/vendor/__init__.py | 2 +- pcapkit/vendor/pcapng/__init__.py | 6 +- pcapkit/vendor/pcapng/tls_key_label.py | 163 ++++++++++++++++++ tests/const/test_const_enum_builtin_parity.py | 15 +- tests/const/test_const_registry_protocol.py | 33 +++- tests/protocols/misc/test_pcapng_unit.py | 33 +++- 14 files changed, 379 insertions(+), 41 deletions(-) create mode 100644 pcapkit/const/pcapng/tls_key_label.py create mode 100644 pcapkit/vendor/pcapng/tls_key_label.py diff --git a/docs/source/pcapkit/const/pcapng.rst b/docs/source/pcapkit/const/pcapng.rst index c044996f22..7291f614c2 100644 --- a/docs/source/pcapkit/const/pcapng.rst +++ b/docs/source/pcapkit/const/pcapng.rst @@ -24,6 +24,8 @@ enumerations include: - Secrets Types [*]_ * - :class:`PCAPNG_FilterType ` - Filter Types [*]_ + * - :class:`PCAPNG_TLSKeyLabel ` + - TLS Key Log Labels [*]_ Block Types =========== @@ -116,6 +118,19 @@ which is automatically generated from :class:`pcapkit.vendor.pcapng.filter_type. :undoc-members: :show-inheritance: +TLS Key Log Labels +=================== + +.. module:: pcapkit.const.pcapng.tls_key_label + +This module contains the constant enumeration for **TLS Key Log Labels**, +which is automatically generated from :class:`pcapkit.vendor.pcapng.tls_key_label.TLSKeyLabel`. + +.. autoclass:: pcapkit.const.pcapng.tls_key_label.TLSKeyLabel + :members: + :undoc-members: + :show-inheritance: + .. raw:: html
@@ -127,3 +142,4 @@ which is automatically generated from :class:`pcapkit.vendor.pcapng.filter_type. .. [*] https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-03.html#name-name-resolution-block .. [*] https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-03.html#name-decryption-secrets-block .. [*] https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-03.html#name-interface-description-block +.. [*] https://www.iana.org/assignments/tls-parameters/tls-sslkeylogfile-labels.csv, c.f., :rfc:`9850#section-4.2` diff --git a/docs/source/pcapkit/protocols/misc/pcapng.rst b/docs/source/pcapkit/protocols/misc/pcapng.rst index 07f1f69268..6b306d5360 100644 --- a/docs/source/pcapkit/protocols/misc/pcapng.rst +++ b/docs/source/pcapkit/protocols/misc/pcapng.rst @@ -197,10 +197,12 @@ Auxiliary Data :undoc-members: :show-inheritance: -.. autoclass:: pcapkit.protocols.misc.pcapng.TLSKeyLabel - :members: - :undoc-members: - :show-inheritance: +:class:`TLSKeyLabel ` moved to +:mod:`pcapkit.const.pcapng.tls_key_label` (GitHub issue #886), generated the +same way as its :mod:`pcapkit.const.pcapng` siblings since :rfc:`9850#section-4.2` +makes it an IANA registry rather than a hand-picked helper enum. This module +still re-exports it under its own name, so +``from pcapkit.protocols.misc.pcapng import TLSKeyLabel`` keeps working. .. autoclass:: pcapkit.protocols.misc.pcapng.WireGuardKeyLabel :members: diff --git a/docs/source/pcapkit/vendor/pcapng.rst b/docs/source/pcapkit/vendor/pcapng.rst index 110016f13a..6a6911367e 100644 --- a/docs/source/pcapkit/vendor/pcapng.rst +++ b/docs/source/pcapkit/vendor/pcapng.rst @@ -24,6 +24,8 @@ vendor crawlers include: - Secrets Types [*]_ * - :class:`PCAPNG_FilterType ` - Filter Types [*]_ + * - :class:`PCAPNG_TLSKeyLabel ` + - TLS Key Log Labels [*]_ .. note:: @@ -124,6 +126,18 @@ which is automatically generating :class:`pcapkit.const.pcapng.filter_type.Filte :members: FLAG :show-inheritance: +TLS Key Log Labels +=================== + +.. module:: pcapkit.vendor.pcapng.tls_key_label + +This module contains the vendor crawler for **TLS Key Log Labels**, +which is automatically generating :class:`pcapkit.const.pcapng.tls_key_label.TLSKeyLabel`. + +.. autoclass:: pcapkit.vendor.pcapng.tls_key_label.TLSKeyLabel + :members: LINK + :show-inheritance: + .. rubric:: Footnotes .. [*] https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-03.html#name-standardized-block-type-cod @@ -133,3 +147,4 @@ which is automatically generating :class:`pcapkit.const.pcapng.filter_type.Filte .. [*] https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-03.html#name-name-resolution-block .. [*] https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-03.html#name-decryption-secrets-block .. [*] https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-03.html#name-interface-description-block +.. [*] https://www.iana.org/assignments/tls-parameters/tls-sslkeylogfile-labels.csv, c.f., :rfc:`9850#section-4.2` diff --git a/pcapkit/const/__init__.py b/pcapkit/const/__init__.py index 2361df9bab..ec0cb91049 100644 --- a/pcapkit/const/__init__.py +++ b/pcapkit/const/__init__.py @@ -83,5 +83,5 @@ # PCAPNG 'PCAPNG_BlockType', 'PCAPNG_OptionType', 'PCAPNG_HashAlgorithm', 'PCAPNG_VerdictType', 'PCAPNG_RecordType', 'PCAPNG_SecretsType', - 'PCAPNG_FilterType', + 'PCAPNG_FilterType', 'PCAPNG_TLSKeyLabel', ] diff --git a/pcapkit/const/pcapng/__init__.py b/pcapkit/const/pcapng/__init__.py index 3184eb874d..ec1ab275e5 100644 --- a/pcapkit/const/pcapng/__init__.py +++ b/pcapkit/const/pcapng/__init__.py @@ -25,6 +25,8 @@ - Secrets Types [*]_ * - :class:`PCAPNG_FilterType ` - Filter Types [*]_ + * - :class:`PCAPNG_TLSKeyLabel ` + - TLS Key Log Labels [*]_ .. [*] https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-03.html#name-standardized-block-type-cod .. [*] https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-03.html#name-options @@ -33,6 +35,7 @@ .. [*] https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-03.html#name-name-resolution-block .. [*] https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-03.html#name-decryption-secrets-block .. [*] https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-03.html#name-interface-description-block +.. [*] https://www.iana.org/assignments/tls-parameters/tls-sslkeylogfile-labels.csv, c.f., :rfc:`9850#section-4.2` """ @@ -42,10 +45,11 @@ from pcapkit.const.pcapng.option_type import OptionType as PCAPNG_OptionType from pcapkit.const.pcapng.record_type import RecordType as PCAPNG_RecordType from pcapkit.const.pcapng.secrets_type import SecretsType as PCAPNG_SecretsType +from pcapkit.const.pcapng.tls_key_label import TLSKeyLabel as PCAPNG_TLSKeyLabel from pcapkit.const.pcapng.verdict_type import VerdictType as PCAPNG_VerdictType __all__ = [ 'PCAPNG_BlockType', 'PCAPNG_OptionType', 'PCAPNG_HashAlgorithm', 'PCAPNG_VerdictType', 'PCAPNG_RecordType', 'PCAPNG_SecretsType', - 'PCAPNG_FilterType', + 'PCAPNG_FilterType', 'PCAPNG_TLSKeyLabel', ] diff --git a/pcapkit/const/pcapng/tls_key_label.py b/pcapkit/const/pcapng/tls_key_label.py new file mode 100644 index 0000000000..f4826111ab --- /dev/null +++ b/pcapkit/const/pcapng/tls_key_label.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""TLS Key Log Labels +======================== + +.. module:: pcapkit.const.pcapng.tls_key_label + +This module contains the constant enumeration for **TLS Key Log Labels**, +which is automatically generated from :class:`pcapkit.vendor.pcapng.tls_key_label.TLSKeyLabel`. + +""" + +from aenum import StrEnum + +from pcapkit.corekit.enum import EnumRegistry + +__all__ = ['TLSKeyLabel'] + + +class TLSKeyLabel(EnumRegistry, StrEnum): + """[TLSKeyLabel] TLS Key Log Labels""" + + #: NSS-historical: not a registered label of :rfc:`9850#section-4.2`'s "TLS + #: SSLKEYLOGFILE Labels" registry. Defined by the Mozilla NSS + #: ``SSLKEYLOGFILE`` convention and removed in NSS 3.34; kept here only so + #: that key logs predating :rfc:`9850` still read. + RSA = 'RSA' + + #: Master secret in TLS 1.2 and earlier, c.f., :rfc:`9850#section-4.2`. + CLIENT_RANDOM = 'CLIENT_RANDOM' + + #: Secret for client early data records, c.f., :rfc:`9850#section-4.2`. + CLIENT_EARLY_TRAFFIC_SECRET = 'CLIENT_EARLY_TRAFFIC_SECRET' # nosec B105 + + #: Early exporter secret, c.f., :rfc:`9850#section-4.2`. + EARLY_EXPORTER_SECRET = 'EARLY_EXPORTER_SECRET' # nosec B105 + + #: Secret protecting client handshake, c.f., :rfc:`9850#section-4.2`. + CLIENT_HANDSHAKE_TRAFFIC_SECRET = 'CLIENT_HANDSHAKE_TRAFFIC_SECRET' # nosec B105 + + #: Secret protecting server handshake, c.f., :rfc:`9850#section-4.2`. + SERVER_HANDSHAKE_TRAFFIC_SECRET = 'SERVER_HANDSHAKE_TRAFFIC_SECRET' # nosec B105 + + #: Secret protecting client records post handshake, c.f., + #: :rfc:`9850#section-4.2`. + CLIENT_TRAFFIC_SECRET_0 = 'CLIENT_TRAFFIC_SECRET_0' # nosec B105 + + #: Secret protecting server records post handshake, c.f., + #: :rfc:`9850#section-4.2`. + SERVER_TRAFFIC_SECRET_0 = 'SERVER_TRAFFIC_SECRET_0' # nosec B105 + + #: Exporter secret after handshake, c.f., :rfc:`9850#section-4.2`. + EXPORTER_SECRET = 'EXPORTER_SECRET' # nosec B105 + + #: HPKE KEM shared secret used in the ECH, c.f., :rfc:`9850#section-4.2`. + ECH_SECRET = 'ECH_SECRET' # nosec B105 + + #: ECHConfig used for construction of the ECH, c.f., :rfc:`9850#section-4.2`. + ECH_CONFIG = 'ECH_CONFIG' diff --git a/pcapkit/foundation/reassembly/data/data.py b/pcapkit/foundation/reassembly/data/data.py index 48d9a0f82d..9733ca960d 100644 --- a/pcapkit/foundation/reassembly/data/data.py +++ b/pcapkit/foundation/reassembly/data/data.py @@ -38,13 +38,23 @@ class Completion(StrEnum): against a member instead. It derives from :class:`~pcapkit.utilities.compat.StrEnum`, as - :class:`~pcapkit.protocols.application.httpv1.Type` and - :class:`~pcapkit.protocols.misc.pcapng.TLSKeyLabel` do, which buys two things - a plain :class:`enum.Enum` does not: the value survives + :class:`~pcapkit.protocols.application.httpv1.Type` does, which buys two + things a plain :class:`enum.Enum` does not: the value survives :func:`json.dumps` -- a plain enumeration raises :exc:`TypeError` there, and :meth:`Datagram.to_dict ` hands this field straight out -- and ``datagram.completed == 'timeout'`` works, so the new state can be tested for without importing this class. + :class:`~pcapkit.protocols.misc.pcapng.TLSKeyLabel` used to be a third + precedent for the same :class:`~pcapkit.utilities.compat.StrEnum` base, but + GitHub issue #886 moved its canonical definition to + :class:`pcapkit.const.pcapng.tls_key_label.TLSKeyLabel`, generated like its + :mod:`pcapkit.const.pcapng` siblings: it now derives from :class:`aenum`'s + own ``StrEnum`` (via :class:`~pcapkit.corekit.enum.EnumRegistry`) rather than + from :mod:`pcapkit.utilities.compat`'s version-branched one. Both + properties above still hold for it either way -- ``aenum.StrEnum`` is a + :class:`str` subclass same as the stdlib one -- but ``isinstance``/ + ``issubclass`` against :class:`pcapkit.utilities.compat.StrEnum` no longer + does, which is why it is called out here rather than left silently stale. Warning: Being a :class:`str` whose :attr:`PARTIAL` and :attr:`TIMEOUT` members are diff --git a/pcapkit/protocols/misc/pcapng.py b/pcapkit/protocols/misc/pcapng.py index 70c7200f06..4d0a0fb171 100644 --- a/pcapkit/protocols/misc/pcapng.py +++ b/pcapkit/protocols/misc/pcapng.py @@ -35,6 +35,7 @@ from pcapkit.const.pcapng.option_type import OptionType as Enum_OptionType from pcapkit.const.pcapng.record_type import RecordType as Enum_RecordType from pcapkit.const.pcapng.secrets_type import SecretsType as Enum_SecretsType +from pcapkit.const.pcapng.tls_key_label import TLSKeyLabel as Enum_TLSKeyLabel from pcapkit.const.pcapng.verdict_type import VerdictType as Enum_VerdictType from pcapkit.const.reg.linktype import LinkType as Enum_LinkType from pcapkit.corekit.module import ModuleDescriptor @@ -270,26 +271,27 @@ class PacketReception(enum.IntEnum): PROMISCUOUS = 0b100 -class TLSKeyLabel(StrEnum): - """TLS key log label, c.f., :rfc:`9850`.""" - - #: NSS-historical: not a registered label of :rfc:`9850#section-4.2`'s "TLS - #: SSLKEYLOGFILE Labels" registry. Defined by the Mozilla NSS - #: ``SSLKEYLOGFILE`` convention and removed in NSS 3.34; kept here only so - #: that key logs predating :rfc:`9850` still read. - RSA = 'RSA' - CLIENT_RANDOM = 'CLIENT_RANDOM' - CLIENT_EARLY_TRAFFIC_SECRET = 'CLIENT_EARLY_TRAFFIC_SECRET' # nosec B105 - CLIENT_HANDSHAKE_TRAFFIC_SECRET = 'CLIENT_HANDSHAKE_TRAFFIC_SECRET' # nosec B105 - SERVER_HANDSHAKE_TRAFFIC_SECRET = 'SERVER_HANDSHAKE_TRAFFIC_SECRET' # nosec B105 - CLIENT_TRAFFIC_SECRET_0 = 'CLIENT_TRAFFIC_SECRET_0' # nosec B105 - SERVER_TRAFFIC_SECRET_0 = 'SERVER_TRAFFIC_SECRET_0' # nosec B105 - EARLY_EXPORTER_SECRET = 'EARLY_EXPORTER_SECRET' # nosec B105 - EXPORTER_SECRET = 'EXPORTER_SECRET' # nosec B105 - #: HPKE KEM shared secret used in the ECH, c.f., :rfc:`9850#section-4.2`. - ECH_SECRET = 'ECH_SECRET' # nosec B105 - #: ECHConfig used for construction of the ECH, c.f., :rfc:`9850#section-4.2`. - ECH_CONFIG = 'ECH_CONFIG' +# NOTE: TLSKeyLabel used to be hand-written here. GitHub issue #886: RFC 9850 +# :rfc:`9850#section-4.2` makes its values an IANA registry ("TLS +# SSLKEYLOGFILE Labels", Specification Required) rather than a closed, +# hand-picked helper enum, so it moved to +# :class:`pcapkit.const.pcapng.tls_key_label.TLSKeyLabel`, generated the same +# way as its :mod:`pcapkit.const.pcapng` siblings -- imported above as +# ``Enum_TLSKeyLabel``, matching the ``Enum_*`` alias every one of its seven +# :mod:`pcapkit.const.pcapng` siblings already carries in this file (the +# owner's review on #890: this import was the only one of the eight lacking +# it, because the class used to be *defined* here rather than imported, so +# nothing applied the convention until this move made it an import). The +# assignment below re-exports the same object under the module's own, +# unaliased name, so ``from pcapkit.protocols.misc.pcapng import +# TLSKeyLabel`` keeps working and still resolves to the identical class -- +# not a copy -- that every internal ``Enum_TLSKeyLabel`` reference below uses. +# :class:`WireGuardKeyLabel` below stays hand-written -- it is verified closed +# (draft-ietf-opsawg-pcapng-06 section 4.7's "is one of" four names) and not a +# registry, so filing it under :mod:`pcapkit.const` would misrepresent it as +# one; the ``Enum_*`` alias convention is about imports of const enums, and +# this is a local definition, so it does not apply here. +TLSKeyLabel = Enum_TLSKeyLabel class WireGuardKeyLabel(StrEnum): @@ -5753,7 +5755,7 @@ def _make_secrets_unknown(self, type: 'Enum_SecretsType', secrets: 'Optional[Dat ) def _make_secrets_tls(self, type: 'Enum_SecretsType', secrets: 'Optional[Data_TLSKeyLog]' = None, *, - entries: 'Optional[dict[TLSKeyLabel, OrderedMultiDict[bytes, bytes]]]' = None, + entries: 'Optional[dict[Enum_TLSKeyLabel, OrderedMultiDict[bytes, bytes]]]' = None, **kwargs: 'Any') -> 'Schema_TLSKeyLog': """Make PCAP-NG TLS Key Log secrets. diff --git a/pcapkit/vendor/__init__.py b/pcapkit/vendor/__init__.py index bedd669b2c..8eae956d09 100644 --- a/pcapkit/vendor/__init__.py +++ b/pcapkit/vendor/__init__.py @@ -109,5 +109,5 @@ # PCAPNG 'PCAPNG_BlockType', 'PCAPNG_OptionType', 'PCAPNG_HashAlgorithm', 'PCAPNG_VerdictType', 'PCAPNG_RecordType', 'PCAPNG_SecretsType', - 'PCAPNG_FilterType', + 'PCAPNG_FilterType', 'PCAPNG_TLSKeyLabel', ] diff --git a/pcapkit/vendor/pcapng/__init__.py b/pcapkit/vendor/pcapng/__init__.py index 6d628e00bd..020a12132c 100644 --- a/pcapkit/vendor/pcapng/__init__.py +++ b/pcapkit/vendor/pcapng/__init__.py @@ -25,6 +25,8 @@ - Secrets Types [*]_ * - :class:`PCAPNG_FilterType ` - Filter Types [*]_ + * - :class:`PCAPNG_TLSKeyLabel ` + - TLS Key Log Labels [*]_ .. [*] https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-03.html#name-standardized-block-type-cod .. [*] https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-03.html#name-options @@ -33,6 +35,7 @@ .. [*] https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-03.html#name-name-resolution-block .. [*] https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-03.html#name-decryption-secrets-block .. [*] https://www.ietf.org/archive/id/draft-tuexen-opsawg-pcapng-03.html#name-interface-description-block +.. [*] https://www.iana.org/assignments/tls-parameters/tls-sslkeylogfile-labels.csv, c.f., :rfc:`9850#section-4.2` """ @@ -42,10 +45,11 @@ from pcapkit.vendor.pcapng.option_type import OptionType as PCAPNG_OptionType from pcapkit.vendor.pcapng.record_type import RecordType as PCAPNG_RecordType from pcapkit.vendor.pcapng.secrets_type import SecretsType as PCAPNG_SecretsType +from pcapkit.vendor.pcapng.tls_key_label import TLSKeyLabel as PCAPNG_TLSKeyLabel from pcapkit.vendor.pcapng.verdict_type import VerdictType as PCAPNG_VerdictType __all__ = [ 'PCAPNG_BlockType', 'PCAPNG_OptionType', 'PCAPNG_HashAlgorithm', 'PCAPNG_VerdictType', 'PCAPNG_RecordType', 'PCAPNG_SecretsType', - 'PCAPNG_FilterType', + 'PCAPNG_FilterType', 'PCAPNG_TLSKeyLabel', ] diff --git a/pcapkit/vendor/pcapng/tls_key_label.py b/pcapkit/vendor/pcapng/tls_key_label.py new file mode 100644 index 0000000000..3f550dac2a --- /dev/null +++ b/pcapkit/vendor/pcapng/tls_key_label.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +"""TLS Key Log Labels +======================== + +.. module:: pcapkit.vendor.pcapng.tls_key_label + +This module contains the vendor crawler for **TLS Key Log Labels**, +which is automatically generating :class:`pcapkit.const.pcapng.tls_key_label.TLSKeyLabel`. + +""" + +import collections +import csv +import re +import sys +from typing import TYPE_CHECKING + +from pcapkit.vendor.default import Vendor + +if TYPE_CHECKING: + from collections import Counter + from typing import Callable + +__all__ = ['TLSKeyLabel'] + +#: Hand-carried comment for the ``RSA`` member, copied verbatim from the text +#: GitHub issue #883 gave it in +#: :class:`pcapkit.protocols.misc.pcapng.TLSKeyLabel` -- ``RSA`` has no row in +#: :attr:`TLSKeyLabel.LINK`'s registry to generate a comment from (``grep -c +#: RSA`` on the fetched CSV is 0), so it is carried across rather than +#: derived, per GitHub issue #886's first constraint. The embedded +#: ``\n #: `` continuations match every other multi-line member comment +#: this crawler (and its siblings) emit, so the rendered class body indents +#: correctly once :meth:`TLSKeyLabel.context` joins it in. +RSA_COMMENT = ('NSS-historical: not a registered label of :rfc:`9850#section-4.2`\'s "TLS\n' + ' #: SSLKEYLOGFILE Labels" registry. Defined by the Mozilla NSS\n' + ' #: ``SSLKEYLOGFILE`` convention and removed in NSS 3.34; kept here only so\n' + ' #: that key logs predating :rfc:`9850` still read.') + +LINE = lambda NAME, DOCS, ENUM, MODL: f'''\ +# -*- coding: utf-8 -*- +# pylint: disable=line-too-long,consider-using-f-string +"""{(name := DOCS.split(' [', maxsplit=1)[0])} +{'=' * (len(name) + 6)} + +.. module:: {MODL.replace('vendor', 'const')} + +This module contains the constant enumeration for **{name}**, +which is automatically generated from :class:`{MODL}.{NAME}`. + +""" + +from aenum import StrEnum + +from pcapkit.corekit.enum import EnumRegistry + +__all__ = ['{NAME}'] + + +class {NAME}(EnumRegistry, StrEnum): + """[{NAME}] {DOCS}""" + + {ENUM} +''' # type: Callable[[str, str, str, str], str] + + +class TLSKeyLabel(Vendor): + """TLS Key Log Labels""" + + #: Link to registry. + LINK = 'https://www.iana.org/assignments/tls-parameters/tls-sslkeylogfile-labels.csv' + + def count(self, data: 'list[str]') -> 'Counter[str]': + """Count field records. + + Args: + data: CSV data. + + Returns: + Field recordings. + + """ + reader = csv.reader(data) + next(reader) # header + return collections.Counter(map(lambda item: self.safe_name(item[0]), reader)) + + def process(self, data: 'list[str]') -> 'tuple[list[str], list[str]]': + """Process CSV data. + + Args: + data: CSV data. + + Returns: + Enumeration fields and missing fields. ``RSA`` is prepended by + hand -- see :data:`RSA_COMMENT` -- since it has no row in the + registry itself. The second element is always empty: this + registry does not mint an ``_missing_`` (see + :class:`pcapkit.const.pcapng.tls_key_label.TLSKeyLabel`'s own + docstring for why), so there is nothing to render there. + + """ + enum = [ + f"#: {RSA_COMMENT}\n RSA = 'RSA'", + ] # type: list[str] + + reader = csv.reader(data) + next(reader) # header + for item in reader: + label, desc, ref = item[0].strip(), item[1].strip(), item[2].strip() + + # NOTE: fails loudly rather than guessing. A silent fallback to + # '9850' here would mis-cite a future row referenced by anything + # other than a bare ``[RFCnnnn]``, and blindly appending + # ``#section-4.2`` below to whatever number *did* match would + # mis-cite a row citing a different RFC entirely -- section 4.2 is + # specifically RFC 9850's own "TLS SSLKEYLOGFILE Labels" registry + # section, not a fact about any other RFC's structure. All ten + # rows fetched as of 2026-09-28 are ``[RFC9850]``, so this always + # takes the ``rfc == '9850'`` branch today; a future registry + # change either still fits it or raises here for a human to look + # at, rather than rendering a citation nobody checked. + match = re.match(r'\[RFC(\d+)\]\Z', ref) + if match is None: + raise ValueError( + f'{label!r} cites {ref!r}, which this crawler does not know how to ' + f'render as an RFC citation -- extend the parsing rather than letting ' + f'it guess') + rfc = match.group(1) + section = '#section-4.2' if rfc == '9850' else '' + + renm = self.safe_name(label) + sufs = self.wrap_comment(f'{desc}, c.f., :rfc:`{rfc}{section}`.') + + pref = f"{renm} = '{label}'" + # NOTE: mirrors the ``# nosec B105`` bandit suppression every + # such member already carries in + # :class:`pcapkit.protocols.misc.pcapng.TLSKeyLabel` as of #883 -- + # bandit's B105 (hardcoded password string) flags a string + # constant assigned to a name containing ``SECRET``, which eight + # of these ten labels' names do. + if 'SECRET' in label: + pref += ' # nosec B105' + + enum.append(f'#: {sufs}\n {pref}') + return enum, [] + + def context(self, data: 'list[str]') -> 'str': + """Generate constant context. + + Args: + data: CSV data. + + Returns: + Constant context. + + """ + enum, _ = self.process(data) + ENUM = '\n\n '.join(map(lambda s: s.rstrip(), enum)).strip() + return LINE(self.NAME, self.DOCS, ENUM, self.__module__) + + +if __name__ == '__main__': + sys.exit(TLSKeyLabel()) # type: ignore[arg-type] diff --git a/tests/const/test_const_enum_builtin_parity.py b/tests/const/test_const_enum_builtin_parity.py index f58012fd34..b7a374469a 100644 --- a/tests/const/test_const_enum_builtin_parity.py +++ b/tests/const/test_const_enum_builtin_parity.py @@ -366,16 +366,23 @@ def test_the_sweep_size_is_pinned(self) -> None: # dropping its ``IntFlag`` base, one for one, so the sum each of those # counts on stays the same even though the two addends moved. # - # Five of the nine string registries are the application layer one, which + # Five of the ten string registries are the application layer one, which # GitHub issue #732 split into a package: the memberless # pcapkit.const.reg.apptype.apptype.AppType base plus one registry per # transport protocol. It is discovered exactly like a member-bearing # registry, since this sweep is structural and never looks at members. + # The tenth is GitHub issue #886's + # pcapkit.const.pcapng.tls_key_label.TLSKeyLabel, moved here from a + # hand-written class under pcapkit.protocols.misc.pcapng -- it defines + # no ``_missing_`` of its own, so it is not among the three ``value. + # upper()`` outliers #647 fixed above, and it never reached + # tests.const.test_const_enum_get's narrower (IntEnum, IntFlag)-only + # sweep, so that module's own pinned count does not move. self.assertEqual(len(ints), 112) self.assertEqual(len(flags), 6) - self.assertEqual(len(strs), 9) - self.assertEqual(len(self.enums), 127) - self.assertEqual(len({obj.__module__ for obj in self.enums}), 121) + self.assertEqual(len(strs), 10) + self.assertEqual(len(self.enums), 128) + self.assertEqual(len({obj.__module__ for obj in self.enums}), 122) def test_every_registry_rejects_a_negative_value(self) -> None: """The registry-wide form of GitHub issue #647. diff --git a/tests/const/test_const_registry_protocol.py b/tests/const/test_const_registry_protocol.py index ab3c3c0eba..ebca561105 100644 --- a/tests/const/test_const_registry_protocol.py +++ b/tests/const/test_const_registry_protocol.py @@ -721,12 +721,32 @@ class _Str(EnumRegistry, StrEnum): #: :class:`~pcapkit.corekit.enum.EnumRegistry` at all, by the owner's own #: ruling against extending it (GitHub PR #836) -- there is no registry #: protocol here to inherit or shadow. +#: +#: ``pcapkit/const/pcapng/tls_key_label.py`` (GitHub issue #886) is excluded +#: for a different reason than the ten above, worth spelling out since it is +#: not "carries a bespoke get()/register()" -- it does not; its own +#: ``get``/``get_all``/``register``/``register_alias`` are exactly the +#: base's generic implementation, same as every one of the 105 +#: :data:`GENERATED_SAMPLE`-shaped registries below. What excludes it is +#: narrower: this whole census, and its ``class \w+(EnumRegistry, IntEnum):`` +#: regex a few lines down, is scoped to the :class:`~aenum.IntEnum` batch +#: tier 3 of #775 converted. ``TLSKeyLabel`` is :class:`~aenum.StrEnum`-valued +#: -- RFC 9850's labels are strings, matched by value -- and it was generated +#: after that batch closed, so it was never a candidate for it either way. +#: Excluding it here is not a judgement about its own inherited-protocol +#: cleanliness; :mod:`tests.const.test_const_enum_get`, +#: :mod:`tests.const.test_const_enum_lookup` and +#: :mod:`tests.const.test_const_enum_builtin_parity`'s dynamic +#: ``pkgutil.walk_packages`` sweeps already discover and cover it generically +#: -- it is simply outside what this file's historical, IntEnum-only census +#: counts. EXCLUDED_STILL_BESPOKE = frozenset({ 'pcapkit/const/ftp/command.py', 'pcapkit/const/ftp/return_code.py', 'pcapkit/const/http/method.py', 'pcapkit/const/http/status_code.py', 'pcapkit/const/pcapng/option_type.py', + 'pcapkit/const/pcapng/tls_key_label.py', 'pcapkit/const/reg/apptype/apptype.py', 'pcapkit/const/reg/apptype/dccp.py', 'pcapkit/const/reg/apptype/sctp.py', @@ -762,9 +782,9 @@ def test_generated_sample_carries_no_copy_of_the_protocol(self) -> None: self.assertIn("def _missing_(cls, value: 'int')", source) def test_every_generated_const_module_is_accounted_for(self) -> None: - """The full census, measured on this batch rather than assumed: 121 + """The full census, measured on this batch rather than assumed: 122 modules under :mod:`pcapkit.const`, splitting exactly three ways -- - the 6 :data:`CONVERTED` bespoke ones tier 2 already handled, the 10 + the 6 :data:`CONVERTED` bespoke ones tier 2 already handled, the 11 :data:`EXCLUDED_STILL_BESPOKE` deliberately left alone, and the remaining 105 this tier converts. Measured by me on the prior head (commit ``05468a06b``, this batch's own base): of those 121, 6 carried @@ -773,6 +793,13 @@ def test_every_generated_const_module_is_accounted_for(self) -> None: the 6 bespoke StrEnum/AppType files, since a hand-copied docstring is not proof of a shared template) -- 105 is what is left once the 6 converted and the 10 excluded are both taken out. + + 121 became 122 with GitHub issue #886's + ``pcapkit/const/pcapng/tls_key_label.py``, and the 10 excluded became + 11 to hold it -- see the comment directly above + :data:`EXCLUDED_STILL_BESPOKE` for why it lands there rather than + among the 105. 105 itself does not move: the new file is absorbed by + the excluded side of the split, not the generated side. """ const_root = REPO_ROOT / 'pcapkit' / 'const' all_files = sorted( @@ -784,7 +811,7 @@ def test_every_generated_const_module_is_accounted_for(self) -> None: generated = [path for path in all_files if path not in converted_relpaths and path not in EXCLUDED_STILL_BESPOKE] - self.assertEqual(len(all_files), 121) + self.assertEqual(len(all_files), 122) self.assertEqual(len(generated), 105) for relpath in generated: diff --git a/tests/protocols/misc/test_pcapng_unit.py b/tests/protocols/misc/test_pcapng_unit.py index fb2fed2839..8847130e69 100644 --- a/tests/protocols/misc/test_pcapng_unit.py +++ b/tests/protocols/misc/test_pcapng_unit.py @@ -1259,10 +1259,20 @@ def test_pcapng_record_and_secrets_readers_cover_dispatch(self) -> None: self.assertEqual(PCAPNG.__dict__['__secrets__'][SecretsType.TLS_Key_Log], 'tls') def test_tls_key_label_tracks_rfc_9850_plus_documented_rsa_exception(self) -> None: + # GitHub issue #886: the canonical definition moved from a hand-written + # class in this module to a generated one under + # :mod:`pcapkit.const.pcapng.tls_key_label`, with a re-export left here + # so ``from pcapkit.protocols.misc.pcapng import TLSKeyLabel`` keeps + # working. Both names have to resolve to the *same* object, not merely + # to two classes that happen to agree. + from pcapkit.const.pcapng.tls_key_label import TLSKeyLabel as CanonicalTLSKeyLabel from pcapkit.protocols.misc.pcapng import TLSKeyLabel - # RFC 9850 :rfc:`9850#section-4.2`'s "TLS SSLKEYLOGFILE Labels" registry, - # plus the NSS-historical ``RSA`` label this module documents and retains. + self.assertIs(TLSKeyLabel, CanonicalTLSKeyLabel) + + # RFC 9850 :rfc:`9850#section-4.2`'s "TLS SSLKEYLOGFILE Labels" registry + # (10 rows as of 2026-09-28), plus the NSS-historical ``RSA`` label this + # module documents and retains -- it has no row of its own. expected = { 'RSA', 'CLIENT_RANDOM', @@ -1277,11 +1287,30 @@ def test_tls_key_label_tracks_rfc_9850_plus_documented_rsa_exception(self) -> No 'ECH_CONFIG', } self.assertEqual(set(TLSKeyLabel.__members__), expected) + self.assertEqual(len(TLSKeyLabel.__members__), 11) + + # Every member's value pinned exactly -- this is a value-matched + # registry (``TLSKeyLabel(label.upper())`` at + # pcapkit/protocols/schema/misc/pcapng.py:2108), so the value, not + # merely the name, is what callers depend on. + for name in expected: + with self.subTest(name=name): + member = TLSKeyLabel[name] + self.assertEqual(member.value, name) + self.assertIs(TLSKeyLabel(name), member) self.assertIs(TLSKeyLabel('ECH_SECRET'), TLSKeyLabel.ECH_SECRET) self.assertIs(TLSKeyLabel('ECH_CONFIG'), TLSKeyLabel.ECH_CONFIG) self.assertIs(TLSKeyLabel('RSA'), TLSKeyLabel.RSA) + # Matching stays by value, not name -- lower-case never resolves, and + # an unrecognised label still raises rather than minting, unchanged + # from before this class was generated. + with self.assertRaises(ValueError): + TLSKeyLabel('client_random') + with self.assertRaises(ValueError): + TLSKeyLabel('NOT_A_REGISTERED_LABEL') + def test_pcapng_schema_helpers_and_post_process_branches(self) -> None: from pcapkit.const.pcapng.block_type import BlockType from pcapkit.const.pcapng.option_type import OptionType