Skip to content

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

Open
JarryShaw wants to merge 2 commits into
mainfrom
feat/2457-sslv2-dissector
Open

JarryShaw wants to merge 2 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 JarryShaw added the review: needs-changes Cross-review at the current head says changes are required; see the verdict comment label Oct 31, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

Held: Actions queue at 47 (above the hold line). The fix round dispatches when the queue drops below 40.

@JarryShaw

Copy link
Copy Markdown
Owner Author

Member order is in scope for this rework. Group the class body as: properties, public methods (id, read, make, register_xxx), dunders (__index__, __length_hint__), then utilities (_read_xxx before its _make_xxx). Use the old protocols (IPv4, TCP, UDP, ARP) as the reference.

@JarryShaw
JarryShaw force-pushed the feat/2457-sslv2-dissector branch from c910fd4 to 6680816 Compare October 31, 2026 05:44
@JarryShaw JarryShaw added review: pending No verdict for the current head - never reviewed, or the head moved since the last one 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 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

GOOD TO GO (Sonnet, 66808164d). The 443 SSLv2 hellos round-trip byte-exact and the new test fails on the old head; TLS-first ordering holds for the polyglot case; mypy and 92 tests are clean.

  • Nit: one extra blank line before class SSLv3 in tls.py; not blocking.

@JarryShaw JarryShaw added review: good-to-go Cross-review at the current head says ready; CI state is separate and removed review: running A cross-review is in flight against the current head - no verdict yet labels Oct 31, 2026
@JarryShaw

Copy link
Copy Markdown
Owner Author

we need _adopted_length?

@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
- 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.
@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.

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).
@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.

This branch has not been deployed

No deployments
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) review: needs-changes Cross-review at the current head says changes are required; see the verdict comment

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

1 participant