cert-machine · Verify Seal
Prove you held the draft, without showing it
Researchers now hold back directions for fear that a draft pasted into a frontier model comes back as someone else’s paper. A seal lets you prove, later, that you held these exact bytes by this date, and shows nothing until you choose to. Seal before anything goes into a cloud model. One file, Python’s standard library, nothing of the content leaves your machine.
planted forgeries certified
0 of 24
flipped bytes, wrong salts, renamed or added files, edited manifests: every one REFUTED or REFUSED
cases in the battery
42
the honest seals certified, the date rule, the unconfirmed anchors, determinism, the shell one-liner
red controls fired
5 of 5
a deliberately broken opener or verifier is caught every time
digests in the public registry
1
each a git commit date, one of the two anchors; the first is our own, below
in one minute
What a seal is, and when to make one
The worry. You have a draft, a statement, a direction, and you want help from a model, a colleague, a referee. Once the bytes leave your machine you cannot prove you had them first. Terence Tao said as much in his Math 2.0 talk: people hold back.
The seal. On your machine, the tool makes a deterministic archive of the file or folder, draws 32 random bytes (the salt), and computes one number from the two: the digest. The archive and the salt stay with you. Only the digest goes out, to two public anchors nobody here controls: OpenTimestamps, which ties it to a Bitcoin block, and this repository’s registry, where one commit holds it. The digest reveals nothing: without the salt there is nothing to guess, and a 32-byte salt is not guessed.
The reveal, when you choose. Hand over the archive and the salt. Anyone re-derives the digest with one line of shell, without this tool, and reads the anchors’ dates. Sealed file by file, you can show one file, the theorem statement, say, while the notes stay sealed: the manifest names no path and no salt.
What to do today. Seal the folder before the first paste into a cloud model. Keep the bundle somewhere you will find it; a lost salt is an unopenable seal. The commands are three lines, below.
what stays open
What a seal does not say
- It does not prove priority of an idea. It proves possession of exact bytes by a date. Someone else may have held the same idea earlier, sealed or not.
- It does not say the content is correct, or whose it is. A seal of a wrong proof is a seal of a wrong proof; a seal of copied bytes is a seal of copied bytes.
- The date is the anchors’, never yours. The time stamp the tool writes at sealing is a local claim and is labelled so in every output; the only dates a verdict uses are the Bitcoin block’s and the registry commit’s. A claim earlier than every confirmed anchor is REFUSED, never certified.
- A short statement alone can be guessed. One line can be guessed and hashed by someone who already suspects it; the salt defeats that, which is why the digest is of salt and hash together. Seal the whole draft anyway.
- A lost salt is an unopenable seal. Without it the digest proves nothing to anyone, including you. The bundle keeps the salt beside the archive; the backup is yours.
- The registry is as strong as GitHub’s record of a push. A maintainer could backdate a commit locally; that is why the registry is the second anchor and OpenTimestamps the first.
- Other operating systems are the open check. The archive rules leave nothing OS-dependent and the battery re-archives the same files under another locale and time zone, but only macOS has run here; a run on Linux or Windows that re-derives a digest from this page’s rules is the outside check.
how
Three commands, and three more to show one file
python3 seal.py seal <file-or-folder> # -> <name>.seal/ : archive.tar, salt.hex, seal.json, digest.txt; prints the digest
python3 seal.py stamp <name>.seal # OpenTimestamps (ots stamp, free) + the registry line to post
python3 seal.py open <name>.seal [--by YYYY-MM-DD] [--registry <clone of this repository>]
One file at a time (v2). Seal a folder file by file; later, show one file and keep the rest sealed.
python3 seal.py seal2 <folder> # -> <name>.seal2/ : manifest.json, salts.json (private), archive.tar (private), salt.hex, seal.json, digest.txt
python3 seal.py reveal <name>.seal2 <path> # the package that shows ONE file: its bytes, its salt, the manifest, the seal and its anchors
python3 seal.py verify <package> [--by YYYY-MM-DD] [--registry <clone>]
Without this tool. The digest is one line of shell on the bundle (macOS; on Linux, sha256sum for shasum -a 256):
( printf 'verify-seal-v1\n'; xxd -r -p salt.hex; shasum -a 256 archive.tar | cut -c1-64 | xxd -r -p ) | shasum -a 256
( printf 'verify-seal-v2\n'; xxd -r -p salt.hex; shasum -a 256 manifest.json | cut -c1-64 | xxd -r -p ) | shasum -a 256
The battery runs both lines against the Python digest on every build.
the decision
Three words, and what each one needs
- CERTIFIED. The archive and the salt re-derive the recorded digest, and at least one public anchor is confirmed here: the digest is in it and its date is read. With a claimed date, the earliest confirmed anchor is on or before it. For one revealed file: its bytes and its name re-derive the manifest’s entry under its salt, and the manifest’s own seal re-derives; the other files stay unrevealed and undecided.
- REFUTED. The digest does not re-derive, and the input that differs is named: the archive, the salt, or the recorded digest itself, told apart by the component hashes the bundle keeps. For a revealed file: the bytes, the name, the index or the salt do not match the manifest.
- REFUSED. A part is missing or malformed, no anchor is in the bundle, an anchor is not confirmed here (an .ots not yet upgraded, no ots client, no registry clone), or every confirmed anchor is later than the claimed date. The reason says what would make it decidable.
why you can trust this
The commitment, and the forgeries it refused
The commitment. digest = SHA256("verify-seal-v1\n" || salt_32_bytes || SHA256(archive_bytes)). The archive is a tar of the regular files under the path, names relative and NFC-normalised, sorted by their UTF-8 bytes, mtime 0, uid and gid 0, mode 0644, no directories, no symbolic links (a symlink refuses the seal), no compression: the same contents give the same bytes on any machine. The salt is exactly 64 lowercase hex characters and nothing else; a trailing newline refuses, which caught a real bug on the battery’s first run.
One file at a time. Per file, with its own salt s_i: h_i = SHA256("verify-seal-v2-file\n" || s_i || SHA256(bytes_i)) and q_i = SHA256("verify-seal-v2-path\n" || s_i || path_i). The manifest holds only an index and those two hashes per file, in canonical JSON, and is sealed as above. The salts stay private; a reveal publishes one. So the manifest shows how many files were sealed and nothing else, and a revealed salt says nothing about the other files.
The battery (certs/seal-battery.json, written by instruments/seal/battery.py in a temporary folder with a temporary git registry; no network, no ots client, the OpenTimestamps path exercised only as present-but-unconfirmed): 24 planted forgeries, 0 certified; 42 cases; 5 red controls on a deliberately broken opener or verifier, each of which must turn a forgery green under it, or the battery itself fails. The tool’s sha256 is in the record (0198a91da9bc…); a changed tool must re-run the battery.
| planted forgery (v1, whole bundle) | word | what the opener said |
|---|
| one byte flipped in the archive | REFUTED | the digest does not re-derive; what differs is the archive: its sha256 is not the one sealed (a byte, a file or a name inside it differs) |
| wrong salt (64 hex, another value) | REFUTED | the digest does not re-derive; what differs is the salt: salt.hex is not the salt drawn at sealing |
| truncated salt (60 hex) | REFUSED | salt.hex is not exactly 64 lowercase hex characters (60 character(s)) |
| salt with a trailing newline | REFUSED | salt.hex is not exactly 64 lowercase hex characters (65 character(s), ends in a newline) |
| salt missing | REFUSED | salt.hex is missing: without the salt the digest cannot be re-derived (a lost salt is an unopenable seal) |
| archive missing | REFUSED | archive.tar is missing: without the bytes there is nothing to re-derive (the researcher holds it) |
| a file added inside the folder (re-archived under the sealed salt) | REFUTED | the digest does not re-derive; what differs is the archive: its sha256 is not the one sealed (a byte, a file or a name inside it differs) |
| a file removed inside the folder (re-archived under the sealed salt) | REFUTED | the digest does not re-derive; what differs is the archive: its sha256 is not the one sealed (a byte, a file or a name inside it differs) |
| a file renamed inside the folder (re-archived under the sealed salt) | REFUTED | the digest does not re-derive; what differs is the archive: its sha256 is not the one sealed (a byte, a file or a name inside it differs) |
| seal.json's digest edited | REFUTED | the digest does not re-derive; what differs is the recorded digest itself: archive and salt are the sealed ones, so seal.json's digest was edited |
| seal.json's version edited | REFUSED | seal.json version 'verify-seal-v0' is not verify-seal-v1 or verify-seal-v2; this opener reads only those |
| seal.json missing | REFUSED | seal.json is missing; the bundle cannot be opened without the recorded digest |
| planted forgery (v2, one file revealed, or the whole bundle) | word | what the verifier said |
|---|
| the revealed file's bytes differ by one bit | REFUTED | the revealed bytes do not re-derive the manifest's sha256 at index 3 (the name does): the bytes differ from the sealed ones |
| the right bytes revealed under another name | REFUTED | the revealed name does not re-derive the manifest's path_hash at index 3 (the bytes do): the file was sealed under another name |
| the revealed file claimed at another index of the manifest | REFUTED | neither the revealed bytes nor the revealed name re-derive the manifest's entry 0 under this salt: not the file that was sealed there |
| a revealed file absent from the manifest (index beyond it) | REFUTED | reveal.json names index 99, but the sealed manifest holds 4 file(s): no such sealed file |
| another salt for the revealed file | REFUTED | neither the revealed bytes nor the revealed name re-derive the manifest's entry 3 under this salt: not the file that was sealed there |
| the manifest edited (a sha256 changed, canonical form kept): its seal no longer re-derives | REFUTED | the manifest's digest does not re-derive; what differs is the manifest: its sha256 is not the one sealed — nothing under it is decided |
| the manifest re-serialised with spaces (not canonical) | REFUSED | manifest.json is not in canonical form (sorted keys, no spaces, one trailing newline): re-derive it with manifest_bytes |
| the manifest missing from the package | REFUSED | manifest.json is missing |
| another manifest salt | REFUTED | the manifest's digest does not re-derive; what differs is the salt: salt.hex is not the manifest's salt — nothing under it is decided |
| the package's seal.json claims v1 | REFUSED | seal.json version 'verify-seal-v1' is not verify-seal-v2; this verifier reads only verify-seal-v2 |
| one byte flipped in the whole bundle's archive (the manifest intact) | REFUTED | the manifest's seal re-derives but 1 file(s) do not: index 2 (notes/plan.txt): the bytes or the name do not re-derive the manifest's entry |
| one per-file salt replaced in salts.json | REFUTED | the manifest's seal re-derives but 1 file(s) do not: index 0 (données.txt): the bytes or the name do not re-derive the manifest's entry |
- Red control. an opener that certifies a mismatched digest IS caught (a flipped byte comes back CERTIFIED under it) Fired.
- Red control. an opener that ignores the claimed date IS caught (a date before the anchor comes back CERTIFIED under it) Fired.
- Red control. an opener that tolerates a newline after the salt IS caught (bytes.fromhex strips it; the strict rule is what refuses) Fired.
- Red control. a verifier that skips the salted path check IS caught (the right bytes under another name come back CERTIFIED under it) Fired.
- Red control. a verifier that skips the salted file hash IS caught (flipped bytes come back CERTIFIED under it) Fired.
the registry
Digests anchored here
The second anchor is seals/registry.jsonl: one line per digest, appended by the maintainer in one commit on a request through the issue form; tools/seal-registry.py refuses anything but a 64-hex digest and a repeat. The commit’s date is what open --registry <clone> reads.
- Posted 2026-11-05, by the maintainer. 8e794bc681ea3338df608502c86c59fabd1ef77d14b2b8a9d05f815c69ddf562
the first seal
Our own proposal, sealed before it was read
What was sealed. claimbox, the claimant of this machine’s Lean proposals, proposed a lab-claim intake route on 5 November 2026 (its branch intake-2026-11-05, 10 files). Before cert-machine read a line of it, the branch’s files were sealed file by file in a private folder; the digest went to OpenTimestamps and to the registry above in one commit. Then one file was revealed, the route document, and the rest stay sealed. The separation this buys is the one the “same operator” caveat on every ETH page names: the author’s bytes are dated by anchors the judge does not control.
The verdict, re-run on every build of this page (certs/seal-first-use.json, the package seals/reveals/claimbox-intake-2026-11-05): CERTIFIED — this file re-derives its salted hash and its salted name at index 6 of a manifest whose seal re-derives; 1 anchor(s) confirmed; the file was held by 2026-11-05. The other 9 file(s) stay unrevealed and undecided.
- The revealed file. lean/INTAKE.md, 12,329 bytes, index 6 of 10 in the manifest.
- The manifest’s digest. 8e794bc681ea3338df608502c86c59fabd1ef77d14b2b8a9d05f815c69ddf562
- Anchor 1, the registry. Confirmed: commit 0dcc77fb, dated 2026-11-05.
- Anchor 2, OpenTimestamps. Stamped, pending: a Bitcoin attestation takes an hour or two, then ots upgrade reads the block; until then this anchor says nothing.
- Held by. 2026-11-05, the earliest confirmed anchor.
What this does not say: that the route is right, or that it is ours alone. It says that these bytes existed, under this author, by this date, before the judge looked. The rest of claimbox’s proposal is decided the usual way, later, from the bytes.