Skip to content

feat(application): add an SSLv2 dissector - #2466

Merged
JarryShaw merged 7 commits into
mainfrom
feat/2457-sslv2-dissector
Nov 1, 2026
Merged

JarryShaw merged 7 commits into
mainfrom
feat/2457-sslv2-dissector

Conversation

@JarryShaw

@JarryShaw JarryShaw commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Please follow the guide below

What is the purpose of your pull request?

  • feat, adds a feature

Description of your pull request and other information

Adds the SSLv2 dissector (draft-hickman-netscape-ssl-00) as a subclass of SSL, reached on TCP 443 only as the fallback after TLS rejects a record. Part of #2457. Rebased onto main, where #2441 has merged.

  • SSL.id() lists SSLv2. On 443, TLS parses first; SSLv2 is taken only for a complete, length-probed CLIENT-HELLO or SERVER-HELLO, otherwise the payload stays raw.
  • The TLS object that adopts an SSLv2 parse reports its 2- or 3-octet header as length, so packet matches a direct parse.
  • Not bound to 443 directly. pcapkit.const.sslv2 adds HandshakeType and Version; the const census pins move +2.
  • Tests: round trips, hand-built and truncated hellos, 443 negatives, a SERVER-HELLO over 443, and a TLS/SSLv2 polyglot that must stay TLS.

@JarryShaw JarryShaw added feat Pull requests that add a new capability (feat: subject prefix) review: running A cross-review is in flight against the current head - no verdict yet labels Oct 30, 2026
@github-actions github-actions Bot added this to the PyPCAPKit v1.5 milestone Oct 30, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

If v2 then module and class name should be SSLv2, keep the v.

Also, I'm expecting it to be a subclass of an abstract class SSL, sibling to SSLv1 and SSLv3/TLS classes.

@JarryShaw JarryShaw moved this to In review in PyPCAPKit Oct 30, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

Noted — module and class rename to SSLv2 (keeping the v), and making it a subclass of a new abstract SSL base shared with the TLS dispatcher (which covers SSLv3 onward, consistent with the wire-level reasoning on #2441 — SSLv3 and TLS share real parsing code, SSLv2 doesn't, but they can all still hang off one top-level SSL entry point for dispatch purposes). This lines up with the TLS/DTLS restructure already in progress on #2441, which is building that same SSL abstract base and its port-443 entry dispatch. Holding this rename until that lands so SSLv2 subclasses the real thing rather than a guess at its shape, then fixing in the same pass.

@JarryShaw JarryShaw added review: needs-changes Cross-review at the current head says changes are required; see the verdict comment and removed review: running A cross-review is in flight against the current head - no verdict yet labels Oct 30, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

Cross-review (different model, adversarial) came back NEEDS CHANGES. Wire format, message types, and round-trip on well-formed records are all verified correct — the defects are elsewhere:

  1. Missing 'SSL2' entry in pcapkit/all.py — this will fail test_every_aggregated_export_is_listed_or_excluded once CI's Project tests job actually runs.
  2. No SSL2._make_data override (every other similarly-shaped protocol has one) — causes UnknownFieldWarning on every from_data() call and silently drops a following record when two are back-to-back.
  3. SESSION-ID-HIT round-trips as a normalized boolean instead of the raw octet — draft defines "hit" as any non-zero value, so 0x04ff... comes back as 0x0401....
  4. The NGAP "never auto-dispatched" precedent cited for not binding port 443 is false — NGAP IS bound, to SCTP PPID 60/66. Not binding 443 is still probably the right call, just needs a real justification.
  5. Closes #2457 closes scope this PR doesn't cover (confirming SSLv3-TLS1.3 coverage on feat(application): implement TLS/SSL and DTLS record layer + handshake framing #2441, documenting SSLv1 as out-of-scope) — should be a non-closing reference instead.
  6. The round-trip tests only check parsed.data == raw, which can never fail (that's just the stored input echoed back) — needs real bytes(X.from_data(parsed.info)) == wire tests, which is what would have caught 1-3.

Should-fix: five of the seven raw-body message types are specified as sent-encrypted, so decoding them as cleartext on a real encrypted session produces garbage — needs a documented fallback. A few RFC/draft citation and overclaim fixes in the docstrings too (see full review).

Separately: the owner's rename ruling (SSL2→SSLv2, subclassing a new abstract SSL base) is being held until PR #2441's restructure lands, since that's where the SSL base is being built — fixing the bugs above now, renaming in a follow-up pass once #2441 lands.

Dispatching the bug fixes now.

JarryShaw added a commit that referenced this pull request Oct 30, 2026
…entry point

Per the owner's ruling on #2441: TLS should dispatch by version with a
thin abstract base and per-version dispatcher, the same shape HTTP
already uses over HTTPv1/HTTPv2. TLS keeps its record-layer schema and
subclasses SSL as before, but now only identifies the concrete tier --
SSLv3, TLSv1_0, TLSv1_1, TLSv1_2 or TLSv1_3, new thin classes each
fixing alias/id() -- and delegates; the real parse moves to
TLS._read_tls_record, shared unchanged by every tier since SSLv3
through TLS 1.3 share the identical wire format this dissector already
treated generically.

Dispatch: SSLv3/TLSv1_0/TLSv1_1 are named directly by the record's
legacy version; 0x0303 is ambiguous between TLS 1.2 and TLS 1.3, which
freezes its own legacy version at TLS 1.2's value, so that case peeks
the ClientHello/ServerHello's supported_versions extension
(SSL._tls_negotiates) and falls back to TLS 1.2 -- the common case --
when the peek is inconclusive. Any other legacy version also falls
back to TLS 1.2, so _validate_tls_record reports the real rejection
reason rather than this method guessing one.

SSLv2 entry point: _looks_like_sslv2 disambiguates SSLv2's
content-type-less header (2-octet form, or 3-octet escaped/plain) from
a genuine TLS/SSLv3+ record's content-type+0x03 pair, and
TLS._guess_version delegates to SSL2 when it matches. The import is
lazy and ImportError-tolerant, since #2466 (SSL2) is not yet merged;
dispatch falls through to the TLS family unchanged until it is. TLS.id()
deliberately does not list SSL2 -- it is checked against every instance
of this class regardless of which tier it resolved to, and listing it
would make genuine TLS 1.2 traffic match an SSL2 lookup.

DTLS 1.3's new unified record header (#2467) is out of scope and
depends on this landing first; only its legacy-format records, which
reuse the shared wire format, are reachable here.

Refs #2437, #2457, #2467
@JarryShaw
JarryShaw force-pushed the feat/2457-sslv2-dissector branch from 66d7002 to c6b605d Compare October 30, 2026 17:10
@JarryShaw

Copy link
Copy Markdown
Owner Author

Fixed and verified directly (new head c6b605d4e): added the missing pcapkit/all.py export, _make_data override (was dropping back-to-back records), SESSION-ID-HIT now preserves the raw octet, corrected the false NGAP precedent, Closes downgraded to Addresses part of, and the round-trip tests now actually assert something (bytes(from_data(...)) == wire). RECORD-LENGTH=0 and oversized padding now raise cleanly instead of corrupting. One should-fix item (encrypted-body raw-fallback) deliberately not applied — would need touching shared corekit schema machinery, documented as a known limitation instead. 19+ tests passing, 100% coverage, lint/mypy clean.

@JarryShaw JarryShaw added review: running A cross-review is in flight against the current head - no verdict yet review: needs-changes Cross-review at the current head says changes are required; see the verdict comment and removed review: needs-changes Cross-review at the current head says changes are required; see the verdict comment review: running A cross-review is in flight against the current head - no verdict yet labels Oct 30, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

Round 2 review: NEEDS CHANGES. Most of round 1 holds up, but:

  1. PR body's "so this does not close Scope SSLv2 as a separate dissector; confirm SSLv3-TLS1.3 generic coverage #2457" accidentally reads as a GitHub closing keyword — rewording needed.
  2. Real regression: SESSION-ID-HIT's fix (bool→int) added no range check, so 256 wraps to 0x00 (hit becomes miss) and -1/300 wrap too — same bug class as the padding fix, just not caught there.
  3. New false claim: RARP dispatch is by EtherType, not IP protocol number.

Should-fix: make() doesn't raise where its docstring claims for a non-hello data-model body; no hand-built (non-make()-derived) wire vector in the tests, so make/read-shared bugs can slip through; a version-value citation slip (reads as 768, docstring said "3" not "768"); the encrypted-body fallback has a simpler fix available (ForwardMatchField or the schema's own raw buffer) than the "needs corekit changes" excuse claimed.

Dispatching round 3.

@JarryShaw
JarryShaw force-pushed the feat/2457-sslv2-dissector branch from c6b605d to 92b7124 Compare October 30, 2026 18:18
@JarryShaw JarryShaw added review: running A cross-review is in flight against the current head - no verdict yet and removed review: needs-changes Cross-review at the current head says changes are required; see the verdict comment labels Oct 30, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

Verified directly (new head 92b7124fe, still one commit on main): closingIssuesReferences is now empty, so #2457 no longer auto-closes; the SESSION-ID-HIT range check sits in _make_ssl2_server_hello() before the UInt8Field pack, refusing 256/300/-1 rather than wrapping (the read path also has a check, which a single-octet field can never trip, so it's defensive only). Diff stays inside the four SSL2 files plus its test file. The worker did not finish a full-repo run, so the suite rests on CI. Cross-review dispatched.

@JarryShaw

Copy link
Copy Markdown
Owner Author

NEEDS CHANGES. Cross-review on Opus at head 92b7124fe. The author was also a default-model (Opus) worker, so this is not a different-model review; the next round gets Sonnet.

All seven round-3 items are fixed as described. Two blockers sit beside them:

  1. Scope SSLv2 as a separate dissector; confirm SSLv3-TLS1.3 generic coverage #2457 will still auto-close. The commit message's line 51 reads PR body's "does not close #2457" …. GitHub honours closing keywords in commit messages when the commit reaches the default branch, and "does not" doesn't stop close #2457 from matching. closingIssuesReferences: [] can't see this. Since the text is in the existing commit, this round amends it rather than stacking a new commit.
  2. The probe gate broke round trips of made schemas. ssl2_body_selector reads packet.get('probe', b'') (schema/application/ssl2.py:167). A make()-built schema has no probe, so the gate reads it as too short and picks UnknownHandshakeBody. Schema_SSL2.from_dict(made.__header__.to_dict()) now raises Field data resolved to a negative length for every made CLIENT-HELLO and SERVER-HELLO, including plain SSL2(). This was byte-exact in round 2. The fix, validated by the reviewer: gate only when probe is present.

Should fix in the same round:

  • The module docstring and PR body still say hello-tagged ciphertext decodes as the hello's fields.
  • version.py calls the version value "the only" deviation, but RFC 6101 Appendix E lists two.
  • The ssl2.rst page lacks entries for the two new probe helpers that docstrings now link to.
  • The PR body's counts are stale.

Correction to my verification above: I said the read-path SESSION-ID-HIT check "can never trip". It does: it's what refuses make(body=Schema_ServerHello(session_id_hit=256)), which would otherwise pack as 0x00. It needs a test, and without one the protocol module's coverage is 99%, not 100%.

@JarryShaw JarryShaw added review: needs-changes Cross-review at the current head says changes are required; see the verdict comment and removed review: running A cross-review is in flight against the current head - no verdict yet labels Oct 30, 2026
@JarryShaw
JarryShaw force-pushed the feat/2457-sslv2-dissector branch from 92b7124 to a3646c4 Compare October 30, 2026 18:58
@JarryShaw

Copy link
Copy Markdown
Owner Author

Verified (a3646c4b0, one commit): no closing keyword left in the commit message; the probe gate applies only when probe is present (ssl2_body_selector), so made schemas round-trip. Sonnet review dispatched.

@JarryShaw JarryShaw added review: running A cross-review is in flight against the current head - no verdict yet and removed review: needs-changes Cross-review at the current head says changes are required; see the verdict comment labels Oct 30, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

Yes, it's needed. _adopted_length (tls.py L112) is set when an SSLv2 record is adopted on 443 (L390), and length returns it (L136-137). Without it an adopted 2- or 3-octet SSLv2 header is reported as TLS's 5, so the packet split and the rebuild are wrong. It is set on an instance, so the class default stays None for plain TLS.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Local-only commit c6b605d is a parallel SSLv2 implementation on main. Rebase onto origin 6680816 is empty: origin already has the same fixes. Nothing to push. Origin head stays good-to-go.

@JarryShaw JarryShaw added review: pending No verdict for the current head - never reviewed, or the head moved since the last one and removed review: good-to-go Cross-review at the current head says ready; CI state is separate labels Oct 31, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

GOOD TO GO (Sonnet, 7fe559299). The commit adds one line to ANCHORS in tests/project/test_conventions_doc_claims.py, matching the index.rst toctree. Narrow tests on this head: conventions doc claims 35 run (1 skipped), SSLv2 32, DTLS/TLS/SSL 60, all OK. The same mismatch exists on main, so the same one-line fix was pushed to main as test-only. Merge is yours.

@JarryShaw JarryShaw added review: good-to-go Cross-review at the current head says ready; CI state is separate and removed review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Oct 31, 2026
@JarryShaw
JarryShaw force-pushed the feat/2457-sslv2-dissector branch from 7fe5592 to 3b5ef6b Compare October 31, 2026 15:22
@JarryShaw JarryShaw added review: pending No verdict for the current head - never reviewed, or the head moved since the last one and removed review: good-to-go Cross-review at the current head says ready; CI state is separate labels Oct 31, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

Rebased onto main after #2443/#2491 landed, new head 3b5ef6ba0. Conflicts were only in the const census tests (builtin_parity, enum_get, enum_lookup, registry_protocol), resolved by accepting this branch's +1 counts for SSLv2's two hand-curated enums. Narrow tests: SSLv2 32 passed, SSL/TLS/DTLS 60 passed. Re-review needed.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Pushed c4eab6fef: the census pins were one short. sslv2 adds 2 const modules but the branch's bumps were all +1 (confirmed against main: sslv2.HandshakeType/sslv2.Version need +2 everywhere). Bumped the remaining +1 on all 5 assertions (including generated in test_const_registry_protocol.py, which the earlier review missed). All 4 census test files pass clean now (128 tests, 1522 subtests).

Separate from the pins: this PR is also a const-vendor-pairing violation under the house rule (docs/source/contributing/conventions/const-vendor-pairing.rst, no exception). pcapkit/const/sslv2/handshake_type.py and version.py cite a non-crawlable source (the expired Netscape SSLv2 draft) but have no pcapkit/vendor/sslv2/ crawler behind them -- same shape the rule now requires a DATA-attribute crawler for, with pcapkit.vendor.linux.arphrd as the precedent (merged on #2497). Recommend adding pcapkit/vendor/sslv2/{handshake_type,version}.py on the same pattern before merge, rather than tracking it as a deferred issue the way TLS's (#2495) was -- that's the one open violation already on record and we shouldn't add a second knowingly.

@JarryShaw JarryShaw added review: needs-changes Cross-review at the current head says changes are required; see the verdict comment and removed review: pending No verdict for the current head - never reviewed, or the head moved since the last one labels Oct 31, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

Cross-review (Sonnet) at c4eab6fef: NEEDS CHANGES. Census pins confirmed clean (128 tests, 1522 subtests) and the SSLv2 dissector itself checks out against draft-hickman-netscape-ssl-00 (message types, record framing, version negotiation, ciphertext-hello guard) -- no implementation defects found.

The blocking item is the const-vendor-pairing gap flagged above: confirmed independently, no pcapkit/vendor/sslv2/ exists and nothing in pcapkit.vendor.__all__ mentions it. Checked the owner's ruling directly on #2495: 'ALL const MUST have its own vendor crawler... no exception.' #2495 itself is reopened, not resolved by deferral, so there's no precedent for waiving this one the same way. Needs pcapkit/vendor/sslv2/{handshake_type,version}.py in the DATA-attribute shape (precedent: pcapkit.vendor.linux.arphrd, #2497) before this merges, and the "hand-curated rather than generated" docstring framing in `handshake_type.py" should go since that's the exact argument the ruling overruled.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Fixed the const-vendor-pairing violation flagged against pcapkit/const/sslv2/handshake_type.py and version.py (#2495).

Added, mirroring the ARPHRD precedent (#2497) exactly:

  • pcapkit/vendor/sslv2/handshake_type.py, version.py — Vendor subclasses whose request() takes no args and returns a module-level DATA dict transcribed from the expired Netscape SSLv2 draft (draft-hickman-netscape-ssl-00), since that draft is not a live, crawlable registry.
  • pcapkit/vendor/sslv2/__init__.py — package wiring (SSLv2_HandshakeType, SSLv2_Version).
  • Wired both into pcapkit/vendor/__init__.py's __all__.

No-op verification: ran the crawlers (python -m pcapkit.vendor.__main__ sslv2) against the committed const files. The only drift was the module docstring (hand-curated "exempt" prose → the templated "automatically generated from..." form, with the draft explanation moved into the crawler's own docstring, same as ARPHRD) and Version.SSLv2's literal (0x0002 → 2, since the crawler renders values in decimal, same as ARPHRD's). Regenerated both const files from the crawlers and confirmed a second run reproduces that output byte-for-byte — true no-op.

Updated for consistency (same stale "hand-curated, no crawler" argument the owner's ruling overruled):

  • docs/source/pcapkit/const/sslv2.rst
  • Docstring/comment in tests/const/test_const_sslv2_unit.py and tests/const/test_const_enum_builtin_parity.py
  • tests/vendor/test_vendor_missing_body_unit.py's EXPECTED_CRAWLERS_WITH_CONSTANT_MISS_LINES pin, 35 → 37, for the two new crawlers' constant _missing_ bodies

Tests: tests/vendor/, tests/const/, tests/protocols/application/test_sslv2_unit.py, and tests/project/test_public_api.py all green — 618 passed (26,710 subtests) on the targeted re-run. A full tests/project/ sweep also ran clean except for one pre-existing, unrelated failure (test_docs_epilog_links_1936_unit.py, a bare dpkt/pyshark literal in docs/source/contributing/conventions/reassembly-integration.rst that predates this branch and isn't touched by this change).

docs/source/contributing/conventions/const-vendor-pairing.rst doesn't exist on this branch yet (it landed on main via #2497, after this branch forked) — so there's nothing to update here re: the "Known Violation" section; sslv2 simply won't need to be listed there once this rebases past that commit.

- SSLv2 (draft-hickman-netscape-ssl-00) subclasses SSL and joins
  SSL.id(). Its module and class are named SSLv2.
- TLS.read tries TLS first. SSLv2 is taken only when TLS rejects the
  record and the payload is a complete CLIENT-HELLO or SERVER-HELLO that
  passes the hello length probe; anything else stays Raw.
- The TLS object that adopts an SSLv2 parse reports its 2- or 3-octet
  header as length, so packet splits as a direct SSLv2 parse does.
- TLS.from_data rebuilds an adopted SSLv2 record as SSLv2, so a port-443
  hello round-trips byte-exact through TCP.from_data.
- Add pcapkit.const.sslv2 (HandshakeType, Version) and bump the const
  census pins by +2 over #2441's.

Part of #2457.
sslv2.HandshakeType and sslv2.Version are 2 new const modules, but
the branch's pin bumps were all +1. Each pinned count needed the
second +1:

- test_const_enum_builtin_parity: ints 118->119, enums 134->135,
  distinct modules 128->129
- test_const_enum_get: enums 124->125, covered 121->122
- test_const_enum_lookup: enums 118->119
- test_const_registry_protocol: all_files 128->129, generated 111->112

Verified: all four files pass clean (128 tests, 1522 subtests).
pcapkit.const.sslv2's two modules were hand-curated with no
pcapkit.vendor.sslv2 crawler, violating the const-vendor-pairing rule
(#2495: "ALL const MUST have its own vendor crawler... use the DATA
attribute... no exception").

- Add pcapkit.vendor.sslv2 (handshake_type, version), mirroring the
  ARPHRD precedent (#2497): each Vendor subclass's request() returns a
  DATA dict transcribed from the expired SSLv2 draft
  (draft-hickman-netscape-ssl-00) instead of fetching a URL.
- Wire SSLv2_HandshakeType and SSLv2_Version into
  pcapkit.vendor.__all__.
- Regenerate both const modules from the crawlers. Only drift: the
  docstring (moved to the crawler) and Version.SSLv2's literal
  (0x0002 -> 2, the crawler's decimal rendering). A second run is a
  byte-for-byte no-op.
- Bump EXPECTED_CRAWLERS_WITH_CONSTANT_MISS_LINES 35->37 for the two
  new crawlers.
- Update docs/source/pcapkit/const/sslv2.rst and the now-stale
  "hand-curated, no crawler" comments in two tests.

Verified: tests/vendor/, tests/const/, the sslv2-specific tests, and
tests/project/test_public_api.py all green.
@JarryShaw

Copy link
Copy Markdown
Owner Author

Rebased onto main (57ff1c8c5, the ARPHRD crawler merge) -- old head 76df51f40, new head e427d96cd. GitHub flagged this as CONFLICTING because both this branch and main bumped EXPECTED_CRAWLERS_WITH_CONSTANT_MISS_LINES in tests/vendor/test_vendor_missing_body_unit.py: main to 36 (for ARPHRD), this branch to 37 (for sslv2, off an older 35 baseline). Resolved to 38 (35 base + 1 ARPHRD + 2 sslv2) and verified against the real crawler count before continuing the rebase.

Patch outside the resolved file is unchanged -- git diff --stat after the rebase shows the same 10 files as before, no unrelated drift. Full relevant suite re-run on the rebased head: tests/vendor/, tests/const/, tests/protocols/application/test_sslv2_unit.py -- 618 passed, 26713 subtests, 0 failed.

Copilot AI and others added 2 commits October 31, 2026 21:13
…ctor

# Conflicts:
#	tests/const/test_const_enum_builtin_parity.py
#	tests/const/test_const_enum_get.py
#	tests/const/test_const_enum_lookup.py
#	tests/const/test_const_registry_protocol.py

Co-authored-by: JarryShaw <15666417+JarryShaw@users.noreply.github.com>
…ctor

Co-authored-by: JarryShaw <15666417+JarryShaw@users.noreply.github.com>
@JarryShaw

Copy link
Copy Markdown
Owner Author

Cross-review (Sonnet) at 53341213e: GOOD TO GO. Confirmed the merge brought in main's content cleanly: no conflict markers anywhere in the tree, the sslv2-specific files are byte-identical since before the merge (0 diff vs. the pre-merge head), and the newly-merged-in IGMP/DTLS reassembly content works correctly alongside sslv2's own. Full suite clean: 678 tests/26861 subtests on the targeted set, 2132/10636 on tests/project with no contention noise this run. mergeStateStatus: BEHIND just reflects main moving again since the merge -- not a conflict. Ready for merge -- your call.

Copilot AI and others added 2 commits November 1, 2026 00:04
…ctor

# Conflicts:
#	tests/const/test_const_enum_builtin_parity.py
#	tests/const/test_const_enum_get.py
#	tests/const/test_const_enum_lookup.py
#	tests/const/test_const_registry_protocol.py
#	tests/vendor/test_vendor_missing_body_unit.py

Co-authored-by: JarryShaw <15666417+JarryShaw@users.noreply.github.com>
…ctor

# Conflicts:
#	tests/project/test_rfc_constants_one_home_1739_unit.py

Co-authored-by: JarryShaw <15666417+JarryShaw@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat Pull requests that add a new capability (feat: subject prefix)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants