Repository navigation
feat(application): implement TLS/SSL and DTLS record layer + handshake framing - #2441
Conversation
|
Coverage: 94.50% (unit tier, Python 3.15,
Per-file detail: the |
|
Review: NEEDS CHANGES at Must fix:
Should fix: TLS 1.2 encrypted Finished (content type 22) currently raises Confirmed correct, no change needed: the shared Confirmed the regression-catching claim independently too, though the real number is higher than claimed: breaking handshake dispatch fails 7 tests, not 3 — the two round-trip tests stay green even with zero decoding, since raw bytes trivially round-trip. One item flagged but not resolved by the reviewer: the dumpkit plist/JSON escaping failures on this PR's CI are a separate, already-tracked regression (#2442, from the just-released dictdumper 0.8.5) — unrelated to this PR's own changes, already has a fix in flight. |
a8f37f1 to
f5b9c25
Compare
|
All required fixes applied and verified directly (new head |
|
NEEDS CHANGES (cross-review round 2, different model from the author). Round 1's cookie fix and Raw fallback both hold up under real-capture testing (OpenSSL DTLS 1.2 handshake through a loopback proxy, 0/40000 random payloads false-positive). But round 2 introduces its own issues: Required: (1) Should-fix: HandshakeType entries 9/10/25/26 and ContentType 27 are still missing from the registry despite the module claiming full IANA coverage; wrong section anchors for the DTLS record header/DTLSCiphertext (cited as §4.1, actually §4); the DTLS docstring's claim that 5684 is "the one fixed port a DTLS-borne protocol is registered at" is false (IANA lists several others: 853, 5349, 6514, 10161/10162, 4740); RFC 8446 should be RFC 9846 (obsoleted July 2026) throughout; the validator is measurably too strict on TCP-segment-split records (a real OpenSSL PQ ClientHello at 1546 bytes exceeds a 1460-byte MSS, so its first segment always falls back to Raw) — worth documenting if not fixing. On the deferred content-type-22 item: the reviewer wrote and tested a ~10-line fix (DTLS: non-zero epoch means encrypted, keep as-is; TLS: keep unless the 4-byte handshake header tiles exactly) that passes all 22 existing tests plus real encrypted Finished/Alert records from a capture. Recommend landing this rather than deferring further, or opening a tracking issue with the corrected reasoning (the original deferral cited the wrong content-type number — 22 is Handshake, not Application Data). Everything else (cookie correctness, pep.rst's NGAP paragraph, scope) verified clean. Full regression: 690/690 passed after generating sample captures (the 5 apparent failures were a fresh-worktree |
f5b9c25 to
1032e28
Compare
|
All required fixes applied and verified directly (new head |
|
NEEDS CHANGES (cross-review round 4, different model from the author). Round 3's fixes hold (content-type-0 allowlist verified across all 256 values; RFC 9846 renumbering independently re-derived and confirmed correct; HandshakeType registry complete against live IANA). Two real problems remain: Required: (1) encrypted Alerts (type 21) still collapse to Raw, the same bug class round 3 just fixed for type 22 Handshake. Any TLS 1.2 Should-fix: a DTLS record with a connection ID (type 25, RFC 9146) is misread — the CID sits before Several accuracy nits in docstrings (overclaiming "always tiles" when RFC 9846 allows split handshake messages; wrong claim about which message types travel under type 22 in TLS 1.3; a GCM-nonce edge case; a non-verbatim RFC 9147 struct quote; dead TYPE_CHECKING import; stale PR-body test counts). CI note: the PR's own red Python 3.11-3.14/Integration-3.15 legs are the same inherited |
1032e28 to
1b2290f
Compare
|
Both required fixes applied and verified directly (new head |
…ecords TLS - TLS.read imports pcapkit.protocols.application.sslv2.SSLv2 (#2466's name); docstrings follow. - _looks_like_sslv2 reads any first octet below 0x40 followed by 0x03 as TLS, so heartbeat/ack/return_routability_check and unaccepted types stay with the TLS validator instead of misparsing as SSLv2. - Tests stub the module under the sslv2 name and cover an SSLv2 CLIENT-HELLO on TCP 443 and every first octet 0-255. - SSL/DTLS docs state where the DTLS epoch and sequence number live; dtls.rst lists the unified-header and RFC 9146 limits; RFC 9846 also obsoletes 5246. - Timed prose removed from pep.rst, ssl.py, dtls.py, sctp.py, ngap.py. TLS/DTLS, application, dispatch, export and docs tests pass.
|
Pushed |
|
NEEDS CHANGES (Sonnet,
|
- TLS.read no longer delegates to SSLv2: remove the guarded import, _looks_like_sslv2, SSL._adopt_dispatched and their tests. A payload that is not a TLS record falls to Raw; SSLv2 delegation lands with its own class. - Pin it: every first octet parses only as an accepted content type, and an SSLv2 hello and random segments on 443 are TCP:Raw. - Give RFC 9846's full obsoletes list; one-line note in SSL.id and ssl.rst that the SSLv2 class joins it in its own PR. - Trim the ssl/tls/dtls docstrings and their rst pages to the rationale; fix the schema's "byte-identical" and drop a timed comment in the dispatch registry test.
|
Verified ( |
|
Heads-up for merge order: this PR and #2466 both bump the |
|
NEEDS CHANGES (Sonnet,
|
Remove "The SSLv2 class joins it in its own PR." from SSL.id() and ssl.rst, and restore the E302 blank line before _udp_to_5684.
|
GOOD TO GO ( |
- Add `pcapkit.foundation.reassembly.dtls.DTLS`, a new `Reassembly`-shaped engine keyed on the UDP 5-tuple plus the DTLS epoch -- the record sequence number resets on an epoch change, so a new epoch opens a fresh buffer rather than folding into the previous one's. It ports the RFC 815 hole-descriptor algorithm `TCP._update_hole_descriptors` already implements, since every fragment declares the message's total length up front (RFC 6347 s4.2.3). A disagreeing fragment (different `length`/`msg_type`) under a reused `message_seq` restarts the message, reporting the displaced partial as its own PARTIAL result rather than dropping it. The default timeout (120s) is an idle deadline from each buffer's most recently received fragment, not pinned to its first -- one buffer holds several independent messages, and RFC 6347 s4.2.4.1's 60s is only a floor a retransmission backoff's cap must not go below, not a safe ceiling. - Wire it into `DTLS.read` via new, opt-in `reassembly`/`flow` arguments: a fragment that completes its message is decoded into a `ClientHello`/ `ServerHello` in place, rather than staying raw. `flow` is required unconditionally once `reassembly` is given, not only for a cleartext handshake record; an encrypted (epoch >= 1) `handshake` record is skipped; and a reassembled message that fails to decode, or whose own fragment header runs past the message's declared length, keeps its raw bytes instead of raising or corrupting the round trip. - Fix the construction path so a reassembled message still round-trips byte-exact: the completing record's fragment header keeps describing that record's own byte range, sliced back out of the full decoded body, rather than writing the full body under a mismatched `fragment_length`. - `DTLSHandshake.is_fragmented` keeps reporting the wire fact that a message was split, even once reassembled -- only `body` changes. - Add the `docs/source/pcapkit/foundation/reassembly/dtls.rst` page and its `reasm.dtls.*` glossary terms, list `DTLS_Reassembly` in `pcapkit.foundation.__all__`/`pcapkit.all.__all__`, fix a record's "at most one fragment" claim (s4.2.3 allows several), and fix three stale RFC citations (s4.2.2 -> s4.2.3 twice; an s4.1 citation overstating that fragment offsets, not just the sequence number, reset on an epoch change). Built against `feat/tls-dtls-2437` (#2441), since `DTLS` does not exist on `main` yet. `isort`, `pylint` (10.00/10 on touched files) and `mypy` (zero new errors vs. base) are clean; `tests/project/` (2102 passed) and `tests/foundation/reassembly/`+`tests/protocols/application/` (499 passed, with `examples/generators/make_samples.py` run first) pass in full.
- 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. - Add pcapkit.const.sslv2 (HandshakeType, Version) and bump the const census pins by +2 over #2441's. Part of #2457.
Summary
Implements TLS/SSL and DTLS (#2437), shaped like ARP:
TLSparses SSLv3 and TLS 1.0–1.3;DTLSparses DTLS 1.0/1.2/1.3 legacy-format records.read()setsalias/namefrom the record's legacy version;id()stays fixed at('TLS',)/('DTLS',).supported_versions. An unknown record version gets the plainTLS/DTLSalias.SSLv3(TLS)exists for explicit construction only, likeInARP(ARP).SSLis the umbrella over the SSL versions, TLS and DTLS. Itsid()is('TLS','DTLS'); the SSLv2 class, and SSLv2 delegation from TCP 443 with its own validation gate, join in feat(application): add an SSLv2 dissector #2466.Raw: 20,000/20,000 random segments do, while 21,504/21,504 valid TLS records still parse as TLS.pcapkit.protocols.application,pcapkit.protocols,pcapkit.allandpcapkit. Constants live inpcapkit.const.tls.Not covered: TCP segment reassembly; DTLS handshake reassembly (#2440); the DTLS 1.3 unified header (#2467); RFC 9146 connection-ID records, which fall back to
Raw.Test plan
test_ssl_tls_dtls_unit.py/test_tls_dtls_roundtrip_unit.py: every version, the branding rules, exports, and byte-exact round trips.Refs #2437