Repository navigation
TLS ClientHello: #2 carried forward with the review fixes (bar v2) - #4
Merged
Merged
Conversation
Review 1 finding 1 and the e4c149a reviews (blocking): parseKS refused a key_share whose client_shares list is empty, and WF required ss <> []. RFC 8446 section 4.2.8 says the vector MAY be empty, which is how a client asks for a HelloRetryRequest, so the refusal was a misreading, and Spec.rejects_key_share_empty_list asked a reviewer to approve it. - Parse.lean: parseKS no longer tests ss.isEmpty. - Value.lean: ExtWF for keyShare no longer requires ss <> []. - Proofs: parseKS_print and parseKS_inv follow; the ExtWF uses in printExtValue_length_le and parseExtValue_printExtValue drop the removed conjunct. - Spec.lean: Spec.rejects_key_share_empty_list becomes Spec.accepts_key_share_empty_list, on the reviewers' 83-byte TLS 1.3 probe (supported_groups, supported_versions 0x0304, key_share 00 00, signature_algorithms). comparator.json names it. - SPEC.md, README: the rule and the table rows say the list may be empty. - tests: the harness oracle accepts an empty list, the named case now expects accept, the probe is a named case, and the generator makes empty lists. TEST-RESULTS.md is from the new run. - bar.toml: zero-aska added to spec_authors, since the spec files changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
Review of e4c149a, blocking item 2: lean4lean rejected ClientHello.Proofs.parseBody_print with "(kernel) deep recursion detected", so lake comparator --paranoid failed (bar item 3). parse_print failed the same way once parseBody_print was set aside. Cause: the proofs relied on the kernel seeing, by unfolding, that parseBody (1 :: (be24 L ++ F)) is the getU24 step that follows. Even a bare rfl of that equation, with F a free variable, sends lean4lean into deep recursion. While it looks for the match it compares unrelated reads such as getU8 (1 :: X) and getU24 X, and so 1 against u24 arithmetic on symbolic bytes. The other five kernels give up on that branch quickly. Fix: parse, parseBody and parseBodyFields now read their fields with four continuation readers, withU8, withU16, withU24 and withBytes (Parse.lean). Each reads a field off the front of the input and calls the rest of the parser with the value and the remaining bytes. Their lemmas (withU16_be16, withU24_be24, withBytes_app, withBytes_self and the matching _inv lemmas in RoundTrip.lean) are proved once for any continuation. So every step of parseBodyFields_print, parseBody_print and parse_print is now a rewrite, and the kernel checks only beta steps and small lemmas. The inverse proofs use the _inv lemmas in the same way. getU8, getU24 and getBytes are no longer used and are removed, and bar.toml's code list names the new readers. The parser accepts and refuses exactly the same inputs as before: the test run gives the same 4,845 inputs and 0 failures, and TEST-RESULTS.md is unchanged. lake comparator --paranoid now passes: Lean's kernel, leanchecker-paranoid, lean4lean, nanoda, con-leche and con-ron all accept the 29 theorems. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
Review 1 finding 2, carried in the e4c149a reviews (should-fix): parse read the record length as any 16-bit value and WF allowed body fields up to 65531 bytes, so a 16385-byte record was accepted. RFC 8446 section 5.1 says the length MUST NOT exceed 2^14 bytes, and a receiver answers a longer record with record_overflow. - Parse.lean: parse refuses a record length above 16384, before it reads the body. - Value.lean: WF bounds the printed body fields by 16380, so the handshake message (4 header bytes and the fields) fits 16384. - Proofs: parse_print and print_parse carry the new bound. - Spec.lean: Spec.rejects_record_overflow states that for every content type, version bytes and following bytes, a length field above 16384 gives none. It is proved in Proofs/SpecProofs.lean and named in comparator.json. - SPEC.md and README state the rule, the theorem and the new WF bound, and SPEC's slice boundaries say that a hello split across records, or too large for one, is refused. - tests: the oracle refuses lengths above 16384; named cases at exactly 16384 (accepted) and 16385 (refused); 60 generated padded hellos within a few bytes of the limit on both sides. The run gives 4,907 inputs and 0 failures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
Review 1 finding 3, carried in the e4c149a reviews (should-fix): the parser checked only that 0 is among the compression methods, so legacy_compression_methods 01 00 with supported_versions [0x0304] was accepted. RFC 8446 section 4.1.2 says that for every TLS 1.3 ClientHello the vector MUST contain exactly one byte, set to zero, and a server MUST abort with illegal_parameter otherwise. SPEC.md listed the weaker rule as a pinned reading, but for TLS 1.3 the RFC leaves nothing open. - Value.lean: new offersTls13, true when a supported_versions extension lists 0x0304. WF requires offersTls13 exts = true -> comps = [0]. - Parse.lean: parseBodyFields refuses a hello that offers TLS 1.3 with any other compression list. A hello that does not offer TLS 1.3 keeps the RFC 5246 section 7.4.1.2 rule, which says the list must contain the null method. - Proofs: the round trip and the inverse lemmas carry the new conjunct. - Spec.lean, proved in Proofs/SpecProofs.lean and named in comparator.json: - Spec.tls13_compression_null: every accepted hello that offers TLS 1.3 has exactly the compression methods [0]. - Spec.rejects_tls13_compression: the hello of Spec.accepts_supported_versions with compression methods [1, 0] is refused. - Spec.accepts_tls12_compression: a hello with no extensions and compression methods [1, 0] is accepted, so the rule is tied to TLS 1.3 and not applied to every hello. - bar.toml: the code list names offersTls13 and the fields the new statement reads (Message.hello, Hello.extensions, Hello.compressionMethods). - SPEC.md and README state the rule, its RFC source and the new theorems. The pinned readings now say only what "offers TLS 1.3" means. - tests: the oracle applies the rule; named cases cover 01 00 and 00 00 with TLS 1.3 (refused) and 01 00 without it (accepted). The run gives 4,911 inputs and 0 failures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
Review 1 finding 4, carried in the e4c149a reviews (should-fix): pre_shared_key (type 41) was carried opaque with only the duplicate check, so a hello with it before key_share was accepted. RFC 8446 section 4.2 says it MUST be the last extension in the ClientHello, and section 4.2.11 says servers MUST check that and fail with illegal_parameter otherwise. A consumer that computes PSK binders over the bytes before it relies on the rule. - Parse.lean: parseBodyFields refuses an extension list in which type 41 stands anywhere but last (41 not in the types of exts.dropLast). - Value.lean: WF has the same condition. - Proofs: the round trip and the inverse lemmas carry it. - Spec.lean, proved in Proofs/SpecProofs.lean and named in comparator.json: - Spec.psk_last: in every accepted hello, no extension but the last has type 41. - Spec.accepts_psk_last: a hello with supported_versions and then pre_shared_key is accepted. - Spec.rejects_psk_not_last: the same two extensions in the other order are refused. - SPEC.md and README state the rule and the theorems. SPEC's slice boundaries now say that rules tying one extension to another (for example psk_key_exchange_modes with pre_shared_key, RFC 8446 section 4.2.9) are not checked. - tests: the oracle applies the rule; named cases put a pre_shared_key with an identity and a binder last (accepted), before key_share and first of two (refused); the generator now draws type 41, and it lands both last and not last. The run gives 4,914 inputs and 0 failures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
…mple as one input Review of e4c149a, non-blocking note: seven refusal statements stopped right after the field under test, so a parser without the named rule still refused them, and the README and SPEC tables paired them with general claims that the theorems do not back. Spec.lean, with the same names, proofs (decide +kernel) and comparator.json entries: - rejects_content_type, rejects_handshake_type, rejects_session_id_33, rejects_odd_cipher_suites and rejects_empty_cipher_suites now use the 61-byte hello of Spec.accepts_supported_versions with only that field changed (and the lengths that cover it). - rejects_compression_without_null and rejects_empty_compression now use the 52-byte hello of Spec.accepts_empty_extensions with only the compression methods changed. That hello offers no TLS 1.3, so the TLS 1.3 compression rule does not apply. A Python reading of SPEC.md with one rule removed at a time accepts each of the first six inputs, so each one is refused by its own rule. The empty compression list is the exception: an empty list holds no null method, so the null-method rule refuses it as well. No input can separate the two rules, and the statement's doc comment and the page say so. README "What is proved" and SPEC.md "Properties and their theorems" now split the claims into statements about every input and statements about single inputs. Each single-input claim says what its input is, for example "the hello of Spec.accepts_supported_versions with content type 23 in place of 22 is refused", rather than claiming the rule for every input. A sentence explains that the two round trips together fix the accepted set as the printed forms of well formed messages. The first item of README "What is not proved" now says what is unproved relative to that predicate: that WF matches SPEC.md. A new item says that no theorem names the bytes of each field (review 1 finding 5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
…L hellos Review of e4c149a, non-blocking note, and review 1 finding 6: - check.py left item 5 OPEN because tests/TEST-RESULTS.md had no "- Oracle:", "- Seed:", "- Inputs:" or "- Failures:" lines. The record now opens with them, and with the command that made it. - The record, the harness docstring and the PR said that Python's ssl module cannot build a ClientHello. That is wrong: a client SSLObject over two MemoryBIOs writes its hello on the first do_handshake(), with no network. The harness now does this for 12 fixed settings, two hellos each (TLS 1.2 only, TLS 1.3 only, both; with and without SNI and ALPN; a cipher list, a single group, post-handshake auth). Each of the 24 must be accepted and printed back byte for byte, and is (OpenSSL 3.0.13 here). - The record also said Go is not installed. That is not true on this machine, so it is dropped. The record now says what is run: OpenSSL supplies real accepted hellos, and no outside TLS stack judges the malformed inputs. - The harness oracle was called "an independent implementation". It is a second reading of SPEC.md by the same authors, and the record, the docstring and README "What you must trust" now say so. The run gives 4,938 inputs and 0 failures, with seed 20261026. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
…claim - Review of e4c149a, non-blocking note: check.py left item 6 OPEN because README's status line said "in-progress" (the bar.toml spelling) where the page must say "in progress". It does now. - Review 1 finding 11: SPEC.md's Safety section, the Parse.lean header and the RoundTrip.lean header said that the parser never reads past the end of its input, but no theorem states that. In the list model every reader is a total function, so there is nothing to prove, and the sentence added nothing beyond print_parse. SPEC.md's section is now "Reading past the end". It says no theorem states this, explains why, and names what the theorems do say: print_parse, and the two refusals of lengths that run past the input. The two Lean headers say the same. In Spec.lean, the doc comments of rejects_record_length_longer and rejects_extension_overrun now describe their inputs instead of reading past the end. Statements are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
lake build printed 111 warnings, most of them already present at e4c149a: if_pos and if_neg are deprecated in Lean 4.35 in favour of ite_eq_left and ite_eq_right, which have the same statements, and 13 simp calls named lemmas they did not use. The proofs now use the new names and drop the unused simp arguments. No statement or definition changes. lake build now prints no warnings, all six kernels accept the comparator theorems, and the test run is unchanged (4,938 inputs, 0 failures). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
README's well formedness paragraph had an overlong line and a doubled "and" after the compression rule was added, and one paragraph of SPEC's "Reading past the end" had an overlong line. Wording only, no change in meaning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
The independent check of the carried branch found that Spec.tls13_compression_null took its meaning from offersTls13, a definition in the code under proof. A wrong program could change offersTls13 to look only at the first listed version and still meet every statement, with Spec.lean and the approval hash unchanged; such a program accepts supported_versions [0x0303, 0x0304] with compression methods [1, 0]. The statement now names the extension and the version itself: for an accepted hello, a supported_versions extension in its list whose versions hold 772 (0x0304) means the compression methods are [0]. It is proved from print_parse and a new lemma, offersTls13_of_mem. Under that mutant the statement is false. Spec.rejects_tls13_compression_second adds the refusal with 0x0304 listed second ([0x0303, 0x0304] with [1, 0]), and the tests gain the same named case. README and SPEC tables, comparator.json, the bar.toml comment on offersTls13 (the parser calls it; no statement reads it) and the test record (4,939 inputs, 0 failures) follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
The independent check of the carried branch found that WF and ExtWF, which give Spec.parse_print and Spec.print_parse their meaning, were listed as code under proof, though the parser never calls them. Bar checklist item 4 says a definition that only gives a statement its meaning belongs in the spec files. As it was, a wrong program could tighten the parser and WF together (record length at most 16383, body fields at most 16379) and still meet every statement with Spec.lean and the approval hash unchanged, refusing a legal record of exactly 2^14 bytes. ExtWF and WF now live in Spec/Defs.lean as Spec.ExtWF and Spec.WF, with the same conjuncts. bar.toml lists Spec/Defs.lean among the spec files, so the approval hash pins the predicate, and drops WF and ExtWF from the code list. The TLS 1.3 conjunct is written on the extension itself (a supported_versions extension whose list holds 772 means compression methods [0]), so the spec files read nothing of the parser; offersTls13 stays in the code for the parser alone. The checker's walk now descends into Spec.WF, so the code list also names the field projections it reads and Extension.casesOn, the case split Spec.ExtWF's match compiles to. The proofs open Spec (WF ExtWF) and bridge the parser's offersTls13 test to the new conjunct with offersTls13_of_mem and a new converse, mem_of_offersTls13. Spec.lean's header, README and SPEC.md say where the predicate is and that the approval covers it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
The independent check of the carried branch found that no statement
pinned the accept side of the 2^14 record limit: only
Spec.rejects_record_overflow (refuse above 16384) and a named test case
at 16384 spoke to it, so a parser that refused a legal record of exactly
16384 bytes could meet every statement.
Spec.accepts_record_at_limit says that for every 16324-byte value v, the
hello of Spec.accepts_supported_versions with a padding extension
(type 21, carried opaque) holding v after its supported_versions, and
the lengths that cover it, is accepted and prints back as the same
bytes. That input is 16389 bytes with record length exactly 16384. It is
proved from parse_print with a Spec.WF witness. A parser that stops at
16383 refuses it, so the statement is false for such a program.
README and SPEC tables (now "every input, or every input of one form"),
comparator.json and SPEC.md's record layer ("a length of exactly 16384
is allowed") follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
The independent check of the carried branch found that two single-input claims, rewritten earlier to describe their input exactly, misstated it in SPEC.md's own terms. "supported_versions value has the odd length 3" names a value that is the 4 bytes [3, 3, 4, 3]; only its list length byte is 3. "value has length 0" names the 1 byte [0], whose list length byte is 0. The Spec.lean doc comment for rejects_supported_versions_odd said the value had an odd byte length, but its byte length is 4. The README and SPEC rows, both Spec.lean doc comments and the two named test labels now give the value bytes and say that the list length byte is 3 (odd, with 3 bytes after it) or 0 (no version listed). The statements themselves are unchanged. The test record changes only in those two labels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
The plain-words definition of Spec.WF left out that a server_name entry list, an ALPN name list and a supported_versions list must each be nonempty, which Spec/Defs.lean requires. SPEC.md already states all three in its per-extension rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
The header said a record of exactly 2^14 bytes is accepted whatever bytes fill it, but Spec.accepts_record_at_limit fixes the first 65 bytes and lets only the 16324 padding value bytes vary. Comment only; no statement changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
The org's license policy (mission#9, mission#26) asks each Lake package to set license = "Apache-2.0". The LICENSE file came from main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
orda-aska
approved these changes
Oct 29, 2026
orda-aska
left a comment
Contributor
There was a problem hiding this comment.
Approving head d9932d6.
What I checked
- CI:
bar / baris green on this head. The log showslake buildwith 17 jobs, and both#print axiomslines show only propext, Classical.choice and Quot.sound.- check.py ran online with
--run-tests. Items 2 to 6 PASS. Item 3 sayslake comparator --paranoidaccepts all 37 theorems under every bundled kernel, so the lean4lean "deep recursion" failure onparseBody_printfrom #2 is fixed. - Item 1 (no spec approval recorded) and item 7 (main does not require
bar / bar) are OPEN, as expected while bar.toml saysin-progress.
- check.py ran online with
- Local build:
lake buildon Lean 4.35.0 succeeds with 17 jobs.python3 tests/run_tests.pygives 4,939 inputs, 0 failures, and leavestests/TEST-RESULTS.mdunchanged. - Escape hatches:
ClientHello/,Proofs/,Spec/andMain.leancontain nosorry,admit,axiom,native_decide,implemented_by,extern,unsafeorpartial. The wordopaqueappears only as the extension constructor name. The onlysorrys are the challenge statements inSpec.lean. - Merge: the branch merges cleanly with main.
Earlier findings on #2
- Empty key_share list (blocking): now accepted.
Spec.accepts_key_share_empty_listpins the 83-byte TLS 1.3 probe, andSpec.ExtWF .keyShareno longer requires a nonempty list. - lean4lean (blocking): fixed by the continuation readers, and confirmed by the comparator in CI.
- Record over 2^14: refused.
Spec.rejects_record_overflowcovers every length above 16384, andSpec.accepts_record_at_limitcovers the accept side for any padding bytes. - TLS 1.3 compression:
Spec.tls13_compression_nullholds for every accepted hello and names the extension and 0x0304 itself.rejects_tls13_compression,rejects_tls13_compression_secondandaccepts_tls12_compressiontie the rule to TLS 1.3. - pre_shared_key not last:
Spec.psk_lastholds for every accepted hello, withaccepts_psk_lastandrejects_psk_not_lastas the two orders. - Seven refusals that missed their rule: they now use complete hellos with one field changed.
- WF location:
WFmoved intoSpec/Defs.lean, so the approval hash pins it. - Test record and page: the record lines and status line are in the format the checker reads. The false sentence about Python ssl is gone, and 24 real OpenSSL hellos now round trip. "Never reads past the end" is no longer claimed as proved.
Non-blocking (the PR body lists these as still open)
- No differential mode compares verdicts with OpenSSL or Go on malformed input.
- No SNI byte-class generators and no planted-bug script.
- Citations still use RFC 8446 numbering, not RFC 9846.
- A TLS 1.2 hello with no extensions block is still refused, as a disclosed reading.
- This approval is a code review of the slice. It does not record a spec approval in
bar.toml(bar item 1), and main still needs thebar / barbranch rule (item 7).
Generated by Claude Code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Carries #2 forward, as the note on #2 said. Refs #1.
This branch carries #2 forward. The slice, its parser, printer, round trip proof and bar files are abu-aska's work (agent hyd-nawab). The branch starts at that PR's head, e4c149a, and keeps its two commits exactly as they are: no rebase, no amend, no squash. The fixes come as 16 new commits on top, one per logical change. The first 10 answer the reviews. The next 4 close gaps that an independent check of the first 10 found, and the last 2 close two small items from the final check.
What changed, finding by finding
Blocking: an empty key_share list was refused
RFC 8446 section 4.2.8 lets client_shares be empty; that is how a client asks for a HelloRetryRequest. The parser refused it, and a spec statement asked a reviewer to approve the refusal.
parseKSno longer tests for an empty list (ClientHello/Parse.lean:209), andSpec.ExtWFno longer requires one.Spec.rejects_key_share_empty_listis nowSpec.accepts_key_share_empty_list(Spec.lean:87). It uses the reviewers' 83-byte TLS 1.3 probe (key_share00 00) and says it is accepted and prints back as the same bytes.Blocking: lean4lean rejected
parseBody_print, solake comparator --paranoidfailedReproduced first: lean4lean reported "(kernel) deep recursion detected" at
parseBody_printandparse_print, which relied on the kernel unfolding the parser over a free tail.parse,parseBodyandparseBodyFieldsnow read their fields with four small continuation readers,withU8,withU16,withU24andwithBytes(ClientHello/Parse.lean:65-95). The old readers are removed. The parser accepts and refuses exactly the same inputs; the test record did not change in that commit.withU16_be16,withBytes_app,withU8_invand the rest), andparseBody_print(Proofs/RoundTrip.lean:1894) andparse_print(:1951) are now chains of rewrites with them.Should-fix: a record longer than 2^14 bytes was accepted
parserefuses a record length above 16384 before reading the body (ClientHello/Parse.lean:305), andSpec.WFbounds the body so the printed record fits.Spec.rejects_record_overflow(Spec.lean:158) covers every content type, version bytes and following bytes: a length field above 16384 givesnone.Should-fix: a TLS 1.3 hello with compression methods other than
[0]was acceptedRFC 8446 section 4.1.2. A hello that does not offer TLS 1.3 keeps the RFC 5246 rule that the list must contain the null method.
parseBodyFieldsrefuses a hello that offers TLS 1.3 with any other list (ClientHello/Parse.lean:275).Spec.tls13_compression_null(Spec.lean:52): every accepted hello with a supported_versions extension listing 0x0304, in any position, has compression methods[0].Spec.rejects_tls13_compression(Spec.lean:211) refuses[1, 0]with TLS 1.3, andSpec.accepts_tls12_compression(Spec.lean:94) accepts[1, 0]without it, so the rule is tied to TLS 1.3.01 00and00 00with TLS 1.3 refused,01 00without it accepted.Should-fix: pre_shared_key not last was accepted
RFC 8446 sections 4.2 and 4.2.11.
parseBodyFieldsrefuses type 41 anywhere but last (ClientHello/Parse.lean:272), andSpec.WFhas the same condition.Spec.psk_last(Spec.lean:62) covers every accepted hello.Spec.accepts_psk_last(:101) andSpec.rejects_psk_not_last(:226) are the same two extensions in both orders.Seven refusal statements did not exercise their rule
Spec.accepts_supported_versionswith only one field changed:rejects_content_type,rejects_handshake_type,rejects_session_id_33,rejects_odd_cipher_suitesandrejects_empty_cipher_suites.Spec.accepts_empty_extensions, which offers no TLS 1.3.Test record, status line, and two wrong sentences
tests/TEST-RESULTS.mdopens with- Oracle:,- Seed:,- Inputs:and- Failures:lines, and README.md:3 saysStatus: in progress.lake buildprinted 111 warnings, most already there at e4c149a. It now prints none.Gaps from the independent check
An independent check of the first 10 commits found three gaps, each closed by its own commit:
offersTls13 ... = true, and a mutantoffersTls13that read only the first listed version met all 35 statements. The statement now names the extension and 0x0304 itself, proved withoffersTls13_of_mem(Proofs/RoundTrip.lean:1712). The newSpec.rejects_tls13_compression_second(Spec.lean:217) refuses supported_versions[0x0303, 0x0304]with[1, 0], and the mutant fails it.WFtogether to a 16383-byte record met every statement.WFandExtWFmove to a new spec file,Spec/Defs.lean(:29,:63), with the same conjuncts, and bar.toml lists that file under the spec files.offersTls13stays in the code for the parser alone, bridged byoffersTls13_of_memandmem_of_offersTls13. The newSpec.accepts_record_at_limit(Spec.lean:120) accepts a 16389-byte input with record length exactly 16384 for every 16324-byte padding value, and the mutant fails it.The final check then found two small items, now closed: README's plain-words
Spec.WFlists the three nonempty-list conjuncts (3ed503d), and the Spec.lean record-limit header says only the 16324 padding bytes vary (f3b9f7a, comment only).Final numbers (f3b9f7a)
lake build: 17 jobs, no errors, no warnings. Both#print axiomslines show onlypropext,Classical.choiceandQuot.sound.lake comparator --paranoid: "Your solution is okay!" for all 37 theorems in comparator.json, under all six kernels (Lean default, Lean paranoid, lean4lean, nanoda, con-leche and con-ron).python3 tests/run_tests.py: seed 20261026, 4,939 inputs, 0 failures, and a fresh run reproducestests/TEST-RESULTS.mdbyte for byte. That is 40 named cases, 3,200 generated, 1,308 mutations, 300 random strings, 60 near the 2^14 limit, 24 from OpenSSL and 7 malformed requests.check.py . --offline --run-tests: no FAIL, exit 0.1b29f389...c60c8d.bar.toml lists zero-aska as a spec author next to abu-aska, since the spec files changed. Status stays
in-progress, with no approval entry.What is still open
If abu-aska would rather land #2 with these commits,
git pull https://github.com/Proven-Commons/clienthello claude/clienthello-first-sliceon that branch is a fast-forward, and I'll close this PR.Generated by Claude Code