Repository navigation
First slice: one DER element (X.690), read and written, proved (bar v2) - #2
Merged
Merged
Conversation
The reader accepts exactly a DER encoding of an element, followed by anything, and gives back the element and what follows; the writer writes that encoding; each element and each integer has one encoding. Statements in Spec.lean, definitions written from X.690 (02/2021) in Spec/Defs.lean, proofs in Proofs/. lake comparator --paranoid accepts all ten theorems under every bundled kernel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
showTree nested string concatenations, so a million-deep tree took minutes to print; it now appends each piece to one string. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
The page names each theorem behind each claim, what is not proved (other types' contents, schema-dependent rules, time, Main.lean), what must be trusted, and how to re-check. SPEC.md states the encoding and the accepted elements in plain words, from X.690 and X.680 Table 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
… page wording - ENUMERATED contents follow the INTEGER rules (X.690 8.4), in Spec.PrimitiveOk and in Der.contentsOk, with two new rejects. - encode_eq holds for every tree, so it pins the writer; the new Der.valid and Spec.valid_iff say whether this slice accepts a tree. The tool's protocol gains a valid request. - x690_examples adds a tag number of 201 in the high form, worked by hand; the leading-zero reject now breaks that rule alone. - README and SPEC no longer say the reader accepts exactly DER: it does for framing and four types, and accepts more for other contents, now listed by clause. encoding_unique is described as one element per input. The SET order condition a reader without a schema could check is named, the length bound is stated right, and a citation is fixed. lake comparator --paranoid accepts all 11 theorems under every kernel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
tests/run_tests.py compares every answer of `der serve` with a Python model of X.690 and with Go cryptobyte, Go encoding/asn1, rust-asn1 and OpenSSL, each pinned. A disagreement with an oracle is allowed only for a named reason the model confirms with that oracle's rules. The model also checks that `valid` holds exactly when a tree's encoding reads back as the tree, and reads the tool's encodings back. 1,612,540 inputs, seed 20261027, 0 failures, no oracle left out: every input of up to two octets, every identifier and initial length octet, lengths around each form boundary, tag numbers up to 2^64, every universal tag in both forms, every INTEGER and ENUMERATED of up to two octets, chains nested 100,000 deep, the Wycheproof ECDSA P-256 signatures, and 100,000 random trees, mutated, and spoiled for `valid`. tests/plant_bugs.py plants bugs in Der/Basic.lean and Main.lean and checks the tests catch each one. The README now says the writer and `Der.valid` take time quadratic in the nesting depth, lists `Der.valid`, and bar.toml lists the test files. 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 bar check asks every approved statement to mention the code under proof; encoding_unique spoke of `Spec.der` alone, so CI failed item 3. It now takes the writer's output, `Der.encode v ++ r = Der.encode w ++ s`, which encode_eq shows is `der`. The proof rewrites by encode_eq. lake comparator --paranoid accepts all 11 statements, and bar/check.py passes item 3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
- Lengths of 5 to 126 octets, with a leading zero and with values that wrap at 32 and 64 bits, each followed by contents a wrong reading would fit; padded lengths of random size. Two planted bugs for them. - A failure whenever the model with an oracle's rules does not give that oracle's answer, whether or not the oracle agrees with the tool. - OpenSSL now checks ENUMERATED with d2i_ASN1_ENUMERATED. - A deep `valid` request; the page says valid's time grows about with the cube of the depth. - Spoiled trees are always refused; the oracle descriptions, the BER wording and the writer's claim say no more than is true. 1,613,447 inputs, 0 failures, every oracle in; 37 of 37 planted bugs caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd
orda-aska
approved these changes
Oct 28, 2026
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.
This is the first slice of the plan in #1, which I claimed there. It is built to bar v2, in the same shape as hostname, punycode, bech32 and hpack. Two self-reviews have run on it, the first of the statements against X.690 and X.680 and the second of the tests and the page, each finding checked by a second agent; the changes they led to are listed at the end.
What it does
One DER element (Rec. ITU-T X.690, 02/2021), nested to any depth, read and written on
List UInt8, with the tree typeDer.Element(Spec/Element.lean):Der.decode b: the element at the start of the input, and the octets after it;Der.decodeAll b: the element that is the whole input;Der.encode e, for any tree;Der.valid e: whether this slice accepts a tree;Der.readInt candDer.writeInt z: the contents octets of an INTEGER.The reader checks what DER fixes for one element:
The contents of the other types, the order of SET components and DEFAULT components are left for later slices. A reader with no schema cannot check the last two.
Spec/Defs.leanlists the clauses left unchecked.What is proved
Spec.leanhas 11 statements.lake comparator --paranoidaccepts them under all six kernels bundled with Lean 4.35.0, using onlypropext,Classical.choiceandQuot.sound, in about 20 seconds. There is nosorryoutside the challenge file.Spec/Defs.leanwrites the encoding from X.690:identifier(8.1.2),lengthOctets(8.1.3, 10.1),der,IntegerContents(8.3.1, 8.3.2),integerValue(8.3.3), andValid, the elements this slice accepts. It shares nothing with the code but the tree type.decode_iff:decode b = some (v, rest) ↔ Valid v ∧ b = der v ++ rest. The reader accepts exactly a DER encoding of an element this slice accepts, followed by anything. So it never reads past the end of its input, every element lies inside its parent's contents, and writing back what it read gives the same octets.encoding_unique: two accepted elements whose encodings start the same input are equal, and so is what follows. This is the plan's non-malleability claim. It is about the tree: for the contents of types this slice does not check, two contents octet strings for the same value are two trees.encode_eq,valid_iff: the writer writesderfor every tree, andDer.validdecidesValid.decode_encode,decodeAll_iff: reading what the writer wrote gives the element back, and the whole input is one element exactly when it is an encoding.readInt_iff,writeInt_spec,writeInt_unique:readIntaccepts exactly the contents X.690 allows and gives their two's complement value,writeIntwrites allowed contents, and they are the only ones for that integer.x690_examples,x690_rejects: X.690's examples (lengths 38 and 201, BOOLEAN TRUE, the primitive "Jones" of 8.23.5), a tag number of 201 in the high form worked by hand, and nineteen inputs that each break a rule, among them the two constructed forms of "Jones" that DER forbids.The proofs go through a reader with fuel and a limit: each element must fit in the octets its parent has left, and the fuel never runs out because each element takes at least two octets.
Time
No proof bounds time. The reader is written to be linear in its input: it measures an element's contents with
takeand never the rest of the input, and adds up long tag numbers and lengths by halves. A tree nested 100,000 deep reads in 0.2 s. The writer is not linear in the nesting depth: each level copies what it holds, so its time grows with the square of the depth; writing 10,000 levels takes about 1.6 s and 100,000 about 190 s.Der.validwrites and measures what each level holds, so its time grows about with the cube of the depth: about 3.4 s for 1,000 levels and 38 s for 2,000. The tests time only nested requests, each in a process of its own that must answer within 60 seconds: reading 10,000 and 100,000 levels, writing 10,000, andvalidon 1,000. The page says so.Differential tests
tests/TEST-RESULTS.md: 1,613,447 inputs, seed 20261027, 0 failures, with no oracle left out.A model of X.690 in Python, sharing nothing with the Lean code, must answer X.690's examples as the standard gives them (14 of 14). Every answer of the tool equals the model's. The model reads the tool's encoding back as the tree for every tree the slice accepts (100,536), and for all 200,412 trees asked
valid, the answer holds exactly when the encoding reads back as the tree.Four oracles:
golang.org/x/crypto/cryptobytev0.25.0, pinned bygo.modandgo.sum;encoding/asn1of the installed Go, go1.24.7: not pinned, and the record gives the version;asn1crate 0.24.1, pinned byCargo.lock;Each reads elements with its own reader and checks BOOLEAN and INTEGER with its own readers. encoding/asn1, rust-asn1 and OpenSSL also check NULL; cryptobyte has no NULL reader. OpenSSL also checks ENUMERATED, with
d2i_ASN1_ENUMERATED.Named differences. A disagreement is allowed only for a named reason the model confirms:
read, with the rules the oracle reads by, gives the oracle's answer. Beyond that, for every element each oracle reads (1,085,584), the model with that oracle's rules must give its answer, whether or not it agrees with the tool, and it does for all of them. The largest:Inputs:
validon chains 1,000 deep;Planted bugs:
tests/plant_bugs.pyplants 37 bugs, 31 inDer/Basic.leanand 6 inMain.lean, and compares with the model alone; all 37 are caught (tests/PLANTED-BUGS.md). Among them:validtaking a class of 4, or not looking inside a constructed element.Not proved
These are listed on the page:
Main.lean, which the tests cover.Re-check
Changes from the self-review
Valid, the code, the model and the tests.encode_eqholds for every tree, andDer.validwithvalid_iffsays which trees the slice accepts.x690_examplesadds a tag number of 201 in the high form, worked by hand;x690_rejectsgrows to nineteen inputs with two ENUMERATED ones, and its length with a leading zero octet now breaks that rule alone.encoding_uniqueis about the tree, and state the SET order and the length bound as they are.From the second review, of the tests and the page:
Der.validtakes time growing about with the cube of the nesting depth (3.4 s for 1,000 levels, 38 s for 2,000), not the square, and that the tests time only nested requests; they now askvalidof chains 1,000 deep under the 60-second limit.Spec.derfor every tree, which is the encoding X.690 gives for a tree this slice accepts, apart from the contents of the types it does not check; and that the class can be any number, withDer.validaccepting only 0 to 3 (also inSPEC.mdand the docstring ofSpec/Element.lean). The docstrings ofDer.encode,Spec.derandSpec.encode_eqsay the same.d2i_ASN1_ENUMERATED; with that in OpenSSL's rules, the model still gives OpenSSL's answer on every element, and no difference from the tool is put down to ENUMERATED for it.Review asks
Spec/Defs.lean. Is accepting both forms for the reserved tags right, or should they be refused? One Wycheproof vector, a SEQUENCE retagged as universal 15, shows the effect.Validasks for lengths below 256^126, from 8.1.3.5 c. Is that the right way to state it?🤖 Generated with Claude Code
https://claude.ai/code/session_01AovXVgJBHGiN44TxQM4dcd