Skip to content

feat(gkverify): add ghostkey-verify, verify-only Ghost Key chains for contracts - #212

Merged
sanity merged 7 commits into
mainfrom
feat/ghostkey-verify
Oct 28, 2026
Merged

sanity merged 7 commits into
mainfrom
feat/ghostkey-verify

Conversation

@sanity

@sanity sanity commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Problem

freenet/mail's v0.3.0 inbox contract needs to verify Ghost Key proofs inside contract WASM. The only verifier today is ghostkey_lib, which carries key generation, blinding and rand; its getrandom refuses wasm32-unknown-unknown, so every contract that links it (freenet-bitcoin's inbox does) has to register a never-called custom getrandom backend.

Approach

A new crate, ghostkey-verify 0.1.0 in rust/gkverify, that does verification only:

  • Ed25519: master key -> notary certificate;
  • RSA-PSS (SHA-384, RFC 9474 finalized blind signature) over the ghost key's CBOR encoding, under the notary's RSA key;
  • Ed25519 verify_strict of a payload signature by the ghost key, over raw bytes (the vault's ScopedPayload), so the crate does not depend on ghostkey-common or freenet-stdlib;
  • parsing the notary info JSON (action, amount, delegate-key-created and its month).

Exactness. The master key signs the CBOR re-encoding of the notary payload, so the crate mirrors ghostkey_lib's serde types one for one, including num-bigint-dig's u64_digit BigUint encoding quirk (a top digit below u32::MAX is written as one word, otherwise two). PEM parsing reproduces Armorable::from_armored_string. The deliberate differences each refuse something no genuine certificate contains, and each has a test asserting ghostkey_lib accepts it and this crate refuses it:

  • a certificate for a weak ghost key (the check freenet-bitcoin's inbox already makes after ghostkey_lib's);
  • notary RSA keys outside 2048-4096 bits or e not in {3, 65537} (blind-rsa-signatures' own signing parameters; bounds the cost);
  • RSA signatures that are not below the modulus (ghostkey_lib's rsa 0.8 reduces them; RFC 8017 refuses them).

Bounds.

  • Input over MAX_CERTIFICATE_BYTES (4 KiB, the inbox's bound) is refused before parsing.
  • So is a certificate whose canonical armour would exceed it, so every accepted certificate's to_armored()/to_cbor() parse back to themselves.
  • Inside armour, such a block stops the search where gklib stops.
  • Sequence reservations are capped.
  • A weak master key is refused.

VerifiedGhostkey can only be made by verify().

Dependencies. ed25519-dalek 2 (no default features, serde), rsa, sha2 0.11, ciborium, serde, serde_json (alloc), base64. rsa is =0.10.0-rc.19 because stable 0.9 pulls in rand through num-bigint-dig's prime feature. The 0.10 line uses crypto-bigint and needs only rand_core, which this crate never calls. It is pinned exactly because the release candidate's API moves between candidates. This is the one dependency choice reviewers should weigh. The move to 0.10.0 is tracked in #213.

Testing

  • tests/differential.rs, against ghostkey_lib as the reference: valid chains (fresh RSA keys each time, re-encoding byte-identical), tampered info, wrong master, notary key swapped, notary signature by another master, wrong RSA signature (other notary, wrong message, bit flips, wrong lengths), ghost key swapped, 1,200 seeded random mutations (flips, truncation, insertion, deletion) with agreement plus identical re-encoding wherever both parse, equivalent re-encodings (unknown keys, reordered keys, zero high digits, byte-array keys, trailing bytes), 16 armour variants, gklib's legacy fixtures (cert plus signed message), and a genuine certificate under the production master key. Payload signatures are checked against verify_strict for valid, wrong signer, wrong or empty payload, every byte flipped, S+l malleation, wrong length and a weak key.
  • Mutation-checked: removing BigUint normalization, changing the top-digit rule, or signing the wrong payload bytes each fails the suite.
  • tests/allocation.rs: a CBOR length header can't make parsing reserve memory (a counting allocator; fails without the cap).
  • tests/wasm_vectors.rs writes 18 proof vectors. CI builds wasm-probe/ (a contract-like cdylib with the freenet-bitcoin release profile) for wasm32, checks it has 0 imports, and runs every vector through it under node. That exercises the 32-bit-limb bignum path, which native tests don't reach.
  • CI fails if rand or getrandom appears in cargo tree -e normal,build --target all.
  • An msrv job checks the library at Cargo.toml's rust-version (1.88), native and wasm32.
  • A weekly run catches upstream drift, and Dependabot watches the crate.

Size. CI measures this crate's probe; the comparison with ghostkey_lib is a one-off measurement and isn't committed. I built a probe doing the same work through ghostkey_lib (plus the getrandom custom hook) with the same profile.

Profile ghostkey-verify ghostkey_lib
opt-level z + LTO (freenet-bitcoin's contract profile) 227 KB 213 KB
opt-level s + LTO 233 KB 248 KB
opt-level 3, no LTO, unstripped 1.98 MB 2.14 MB

The ~1 MB figure only appears in an unoptimised-for-size build. Built for size, the two are about the same. The gain is the dependency tree, not bytes.

Deploy. Merging triggers two site workflows. deploy.yml rebuilds GitHub Pages on every push to main; that is an ordinary rebuild of unchanged content, the same as its daily scheduled run. publish-freenet.yml republishes the site to Freenet from main. It ignores rust/**, and this PR adds .github/workflows/ghostkey-verify.yml to its paths-ignore so later CI edits don't trigger it. This merge still triggers it once, because it edits files outside its paths-ignore (publish-freenet.yml itself and .github/dependabot.yml). That run publishes from main with the same site content as the last merge. Nothing in hugo-site/ changes.

After merge: cargo publish of ghostkey-verify 0.1.0 from main.

[AI-assisted - Claude]

https://claude.ai/code/session_01Xxpwzvt4ucaGfbeCr77Jns

… contracts

A new crate, published as ghostkey-verify, that checks a Ghost Key
certificate chain (master -> notary -> ghost key), Ed25519 signatures by the
ghost key over raw payload bytes (the vault's ScopedPayload), and the notary
info JSON. No key generation, no blinding, and neither rand nor getrandom in
its dependency tree, so Freenet contracts can link it for wasm32 without a
custom getrandom backend. freenet/mail's v0.3.0 inbox contract is the first
user.

Parsing and the chain check mirror ghostkey_lib 0.2 type for type, including
the bytes the master key signs (num-bigint-dig's u64_digit BigUint encoding
quirk is reproduced), and differential tests hold the two together. The
deliberate differences each refuse something no genuine certificate
contains: weak ghost keys, notary keys outside 2048-4096 bits / e in {3,
65537}, and RSA signatures not below the modulus.

rsa is the 0.10 release candidate, pinned exactly: stable 0.9 pulls in rand
through num-bigint-dig's prime feature.

CI (ghostkey-verify.yml) runs the tests, checks the dependency tree for rand
and getrandom, builds a contract-like probe for wasm32, checks it imports
nothing, and runs proof vectors through it under node.

Claude-Session: https://claude.ai/code/session_01Xxpwzvt4ucaGfbeCr77Jns
From the first review round on #212:

- Refuse certificates over MAX_CERTIFICATE_BYTES (4 KiB, the inbox
  contract's bound) before parsing, and cap the reservation a CBOR length
  header can cause at 1024 elements: a 50-byte input could make serde
  reserve 1 MiB, which a contract's linear memory never gives back. A
  counting-allocator test fails without either cap.
- Refuse a weak master key, under which the non-strict notary check accepts
  a forged signature; check the weak ghost key before the RSA operation.
- Differential tests for PSS salt lengths other than 48 (the auto salt
  length was untested), for notary keys of 2049, 2050 and 3072 bits and
  e = 3, and for the refused keys (1024, 2047 bits, e = 5), oversized input
  and a weak master. Armour variants pinned one by one.
- The wasm probe returns one code per error; more fresh keys, deep nesting
  in an unknown field and the new refusals run under wasm (16 vectors).
- rust-version 1.88 (sha2 0.11.1 needs it); fix a broken intra-doc link.
- CI: weekly run for upstream drift, clippy on the probe, a verifying
  cargo package, read-only token. publish-freenet.yml ignores this
  workflow's file, which is not in the site.
- README: the exact rsa pin and its cost (tracked in #213), the rsa
  getrandom feature, why ed25519-dalek 2. AGENTS.md and gklib's AGENTS.md
  name the crate and the coupling.

Claude-Session: https://claude.ai/code/session_01Xxpwzvt4ucaGfbeCr77Jns
…meter docs

From the parity review on #212: run notary keys the issuing code never makes
but the crate accepts (e = 3, a modulus that is not a whole number of bytes)
through the wasm probe too, and describe the RSA parameter limits accurately
(blind-rsa-signatures checks them only when parsing DER, by whole bytes; the
modulus must also be odd).

Claude-Session: https://claude.ai/code/session_01Xxpwzvt4ucaGfbeCr77Jns

@sanity sanity left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comprehensive PR Review: #212 (round 1)

Summary

  • PR: feat(gkverify): add ghostkey-verify, verify-only Ghost Key chains for contracts
  • Review tier: Full (cryptographic verification; new CI workflow)
  • HEAD SHA reviewed: 97eb5bf
  • CI on 97eb5bf: green
  • Reviewers run: freenet:code-first-reviewer, freenet:testing-reviewer, freenet:skeptical-reviewer, freenet:big-picture-reviewer, plus two change-specific Claude lenses: (a) byte-exact parity with ghostkey_lib, (b) dependencies, CI and packaging.
  • External model: codex review --base origin/main was tried twice and failed both times with "You've hit your usage limit". Per policy I substituted the two extra Claude lenses above and did not wait.

What the reviews established

  • No Must Fix. No reviewer found an input this crate accepts that ghostkey_lib rejects.
  • The parity lens tested this directly:
    • RSA-PSS on about 1,100 hand-made vectors: key sizes 2040 to 4097 bits including 8k+1 moduli, e = 3 and 65537, every salt length, malformed encodings, s+n and leading-zero signatures. All of them also ran under wasm32.
    • 20,000 CBOR transcodings: non-minimal integer headers, indefinite-length items, chunked strings and tags.
    • Only gklib accepted the documented differences. Nothing was accepted by this crate alone.
  • The skeptical lens checked every unwrap/expect reachable from input and found none that can fire.

Findings and how each was addressed (fixed in ca0f4b7 and a973b0e)

# Finding Severity Source Resolution
1 rust-version = "1.85" is false (sha2 0.11.1 needs 1.88, crypto-bigint 0.7.10 needs 1.87) Should Fix code-first, deps Set to 1.88
2 A 50-byte CBOR header claiming a 2^32-element array makes serde reserve 1 MiB. A contract's linear memory never shrinks. Should Fix skeptical Reservation capped at 1024 elements (bounded_vec, which accepts the same inputs as Vec<T>). A counting-allocator test fails without the cap for both the modulus and the signature.
3 No input bound; the inbox enforces 4 KiB before parsing Should Fix code-first, big-picture, skeptical MAX_CERTIFICATE_BYTES = 4 KiB, exported and enforced in from_armored/from_cbor; documented as a difference, with a test
4 Auto PSS salt length untested (a fixed-48 mutation survived) Should Fix testing Differential test for salt 0, 1, 20, 32, 48, 64, 206; the mutation now fails
5 Notary keys other than 2048/e=65537 never tested end to end Should Fix testing, parity Differential tests for 2049, 2050, 3072 bits and e = 3 (both accept), and for 1024, 2047 bits and e = 5 (gklib accepts, we refuse). e=3 and 2049-bit vectors also run under wasm.
6 Armour variants pinned only as a count range Should Fix testing Each variant now has an exact expected verdict
7 cargo package --no-verify --list cannot go red Should Fix deps, testing, code-first The step now runs a verifying cargo package
8 Merging triggers publish-freenet.yml: the workflow file is outside its paths-ignore. The PR text said otherwise. Should Fix big-picture .github/workflows/ghostkey-verify.yml added to its paths-ignore. This merge still triggers one republish of the site from main, because it edits that workflow; the PR text is corrected.
9 Broken intra-doc link to ghostkey_lib Should Fix big-picture Plain crates.io link
10 AGENTS.md files do not mention the crate or the coupling to gklib Should Fix big-picture Both updated
11 A weak master key would let anyone forge notary certificates under the non-strict check Consider code-first Refused (WeakMasterKey), with a test showing gklib accepts the forgery
12 Weak-ghost-key check after the RSA operation Consider code-first Moved before it
13 Wasm vectors can't tell failing stages apart; only two RSA keys went through wasm Consider testing One code per error; 18 vectors including 5 fresh keys, deep nesting in an unknown field (200 deep accepted, 300 refused), oversized input, weak master
14 Exact rsa rc pin blocks a downstream that uses another rsa 0.10; the getrandom feature can leak in through unification Consider deps, big-picture Documented in the README; move to 0.10.0 final tracked in #213
15 No upstream-drift detection; clippy not run on the probe; no permissions block Consider deps, code-first Weekly schedule, probe clippy, contents: read
16 action not validated; date parser lax Consider code-first, skeptical Docs tell callers to check action; more refusal cases tested. A calendar check is not added because the string is master-signed.
17 Parameter wording ("what blind-rsa signs with") inaccurate; even modulus not listed Consider parity Reworded

Considered and not changed

  • ed25519-dalek 3. It would remove the duplicate sha2/digest/signature stacks, but ghostkey-common, freenet-bitcoin and freenet/mail are on dalek 2, and VerifyingKey is re-exported. Staying on 2 keeps it interoperable with the callers; the README says why.
  • Committed lockfile. The repo ignores Cargo.lock. CI resolving fresh is what a new downstream contract gets, and the weekly run now catches drift.
  • Master signature verify vs verify_strict (a surviving mutation). verify is kept for parity with gklib; a weak master is now refused separately.

Verdict

State: Needs Changes, re-review required after fix. The fixes touch security-relevant parsing and grew the diff by more than 30 lines, so a full re-review runs on a973b0e.

[AI-assisted - Claude]

…esult type

From the second review round on #212:

- A certificate is refused if its canonical armour would exceed
  MAX_CERTIFICATE_BYTES. An RSA signature sent as a CBOR byte string
  re-encodes at twice the size, so before this a certificate could parse
  while its own to_armored() did not. Every accepted certificate's canonical
  forms now parse again, to themselves; the differential helper asserts it
  for every input both parse. The armour length is computed, not encoded.
- VerifiedGhostkey's fields are private (ghost_key(), info()), so only
  verify() makes one and holding one means the chain was checked.
- Tests: the cheap refusals come before the chain (weak keys with every
  signature broken); a 4096-bit notary key end to end, its certificate and
  the worst case inside the size limit; a 4097-bit modulus refused; RSA and
  master signatures as CBOR byte strings.
- CI: an MSRV job checking the library at Cargo.toml's rust-version (native
  and wasm32); main runs are not cancelled by later pushes.
- Dependabot watches rust/gkverify (rsa ignored: moved by hand, #213); the
  root README lists the crate; the README example checks `action`.

Claude-Session: https://claude.ai/code/session_01Xxpwzvt4ucaGfbeCr77Jns

@sanity sanity left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comprehensive PR Review: #212 (round 2, re-review)

  • HEAD SHA reviewed: a973b0e (CI green there). Fixes are in eee220e.
  • Tier: Full.
  • Reviewers run: freenet:code-first-reviewer, freenet:testing-reviewer, freenet:skeptical-reviewer, freenet:big-picture-reviewer, plus a change-specific lens on contract safety and inbox parity.
  • External model: codex review was retried and still reports "You've hit your usage limit". The extra Claude lens substitutes for it, per policy, without waiting.

This is a re-review of the whole diff after the round-1 fixes. Every reviewer confirmed the round-1 fixes are correct, and none found a Must Fix.

The lenses checked these directly:

  • Determinism: 6,000 mutated certificates under 4 masters gave identical verdicts natively and under wasm32, and matched gklib.
  • Hostile input under wasm: nesting 4,000 deep, 2^64 length claims and 2,000-chunk strings were all refused cleanly, with memory steady at 18 pages.
  • Encodings: every encoding variant tried agreed with gklib, including byte strings, chunked strings, tags and bignum tags.
  • Mutation testing: every guard added in round 1 is caught by a test.

Findings and resolution (eee220e)

# Finding Severity Source Resolution
1 from_armored(to_armored(x)) is not total. A ~3.9 KB input with the RSA signature as a CBOR byte string re-encodes to 6.9 KB, which the crate then refuses, so a contract that canonicalizes before verifying gets a form it can't read back. Should Fix contract lens A certificate whose canonical armour would exceed the limit is refused at parse (computed from the CBOR length, with no extra encoding). The differential helper now asserts that canonical forms re-parse to themselves for every input, and a difference test covers the case.
2 VerifiedGhostkey has public fields, so anyone can construct one without verifying Should Fix contract lens Fields are private; ghost_key() and info() accessors. Only verify() makes one.
3 The "weak keys cost no RSA operation" ordering is untested Should Fix code-first, testing cheap_refusals_come_before_the_chain covers weak keys with every signature broken. Moving the check after the chain fails it (checked).
4 No 4096-bit certificate is tested against the 4 KiB cap Should Fix code-first End-to-end 4096-bit test that asserts the armour and the worst-case signature fit
5 The maximum key size isn't pinned (a 4100 mutant survived) Should Fix testing Unit case for a 4097-bit modulus
6 rust/gkverify has no Dependabot entry Should Fix big-picture Added; rsa is ignored because it's moved by hand (#213)
7 The size comparison in the PR isn't reproducible Should Fix big-picture Stated as a one-off measurement, with the full table
8 Byte-string encodings go through the new bounded_vec untested Consider skeptical Covered for the RSA and master signatures
9 Nothing in CI tests the MSRV Consider testing New msrv job checks the library at Cargo.toml's rust-version, native and wasm32
10 Main-branch runs can be cancelled by later pushes; toolchain drift goes unmentioned Consider code-first cancel-in-progress only for PRs; the workflow comment now covers toolchain drift
11 README example skips the action check; the root README omits the crate Consider contract lens, big-picture Fixed
12 The probe's -99 catch-all could hide a new error Consider testing The vector test asserts no expectation is -99
13 The explicit signature-length check is redundant with rsa's Consider testing Kept as the mirrored rule, with a comment saying so

Considered and not changed

  • ClaimedKey newtype for claimed_ghost_key() (contract lens). The docs and README show the required order, and finding 2 makes a verified result unforgeable. A second key type would add API without making the payload check meaningful on its own.
  • Exact rc pin. Documented in the README and tracked in #213.

Verdict

State: Needs Changes, re-review required after fix. Several Should Fix items touched parsing, so a full round 3 runs on eee220e.

[AI-assisted - Claude]

… does

From the third review round on #212 (found independently by two lenses):
the round-2 canonical-size bound made from_cbor fail inside armor::decode's
label loop, which then moved on to the next label. ghostkey_lib has no such
bound and stops at the first block that parses, so text holding an
oversized decoy block followed by a genuine certificate under the
unversioned label was accepted here and refused there: an accept-side
difference, which the crate promises never to have. decode now returns
TooLarge at that block. The difference test covers the two-block case, and
fails if the early return is removed.

Also from round 3:
- canonical_bound_sits_exactly_at_the_limit sweeps byte-string signatures
  across the threshold, landing on 4096 itself, and requires the crate to
  parse exactly when ghostkey_lib's canonical armour fits; it kills the >=
  and CBOR-length mutants that survived before.
- NotaryInfo is #[non_exhaustive] (made only by parse); GhostkeyCertificate
  is Debug; docs on TooLarge, from_cbor's many encodings, production_master.
- The README example returns its own error for a non-donation action rather
  than misusing Error::Info.
- Dependabot no longer ignores rsa, so its advisories are not silenced.

Claude-Session: https://claude.ai/code/session_01Xxpwzvt4ucaGfbeCr77Jns

@sanity sanity left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comprehensive PR Review: #212 (round 3, re-review)

  • HEAD SHA reviewed: eee220e (CI green there, including the new msrv job). Fixes are in 8ccd956.
  • Tier: Full.
  • Reviewers run: freenet:code-first-reviewer, freenet:testing-reviewer, freenet:skeptical-reviewer, freenet:big-picture-reviewer, plus the contract-safety and inbox-parity lens.
  • External model: codex review is still at its usage limit. The extra Claude lens substitutes, per policy.

Findings and resolution (8ccd956)

# Finding Severity Source Resolution
1 Accept-side difference introduced in round 2. The canonical-size refusal inside armor::decode moved on to the next label, while gklib stops at the first block that parses. A 3.9-4.1 KB text with an oversized decoy _V1 block followed by a genuine certificate under the unversioned label was accepted here and refused by gklib and the inbox. Not a forgery (only a genuine certificate is accepted), but it breaks the "differences only refuse" promise. Should Fix skeptical, contract lens (found independently) decode returns TooLarge at that block. The two-block case is in the difference test, and removing the early return fails it.
2 The canonical bound was guarded only at gross scale (the >= and CBOR-length mutants survived), and assert_agree's re-parse check guarded nothing on its own Should Fix testing New canonical_bound_sits_exactly_at_the_limit sweeps byte-string signatures across the threshold, lands on exactly 4096, and runs every parsed case through assert_agree. Both mutants are now killed.
3 NotaryInfo not #[non_exhaustive], so a field added later would be a semver break Should Fix big-picture Done (it is only made by parse)
4 Deploy paragraph incomplete: .github/dependabot.yml also triggers publish-freenet.yml Should Fix big-picture PR text corrected (still one republish from main, no site change)
5 from_* docs and TooLarge docs didn't cover the canonical-size rule Should Fix code-first Documented. TooLarge now surfaces from the armour path too (it no longer collapses to Armor).
6 README example misused Error::Info for a wrong action Consider big-picture Example returns its own error
7 Dependabot ignore: rsa might also silence advisories Consider code-first Ignore removed; candidate bumps are decided by hand
8 GhostkeyCertificate has no Debug; production_master has no Panics note; from_cbor lacks the "store canonical bytes" advice; encoded_len not tested at 0 or near 4096; msrv drift unexplained Consider big-picture, contract lens, skeptical, code-first All done (encoded_len now tested over 0..=3100)

Verified as holding (no change)

  • Round trip. 200k seeded re-encodings natively and 14k under wasm32 (opt-z, LTO, panic=abort): every accepted certificate's canonical forms re-parse to themselves. No case was accepted here and refused by gklib except finding 1, now fixed. Re-encodings match gklib byte for byte.
  • Arithmetic. encoded_len is exact. There is no reachable overflow or panic, canonicalization is idempotent, and VerifiedGhostkey can't be forged.
  • MSRV job. On 1.87 it fails (sha2 0.11.1 requires 1.88); on 1.88 it passes, native and wasm32.

Not changed

  • YearMonth stays constructible, since callers build one to compare against created_month.
  • MIN/MAX_NOTARY_KEY_BITS stay public, as an intended part of the API.

Verdict

State: Needs Changes, re-review required after fix. Finding 1 touched parsing, so round 4 runs on 8ccd956.

[AI-assisted - Claude]

…r under wasm

From the fourth review round on #212 (no Must Fix; no code change here):
- The single-block and two-block oversized cases are separate tests, so
  removing armor::decode's TooLarge return fails each by name.
- A wasm vector whose input fits and whose canonical form does not, so the
  computed armour length runs on 32-bit usize.
- Comments: what refuses an oversized block on the inbox side; that the
  gklib agreement holds for the same resolved ciborium/serde/dalek versions;
  Dependabot's rsa note softened to what is known.

Claude-Session: https://claude.ai/code/session_01Xxpwzvt4ucaGfbeCr77Jns
So the vector's TooLarge comes from the canonical-form bound, not the
input-length check.

Claude-Session: https://claude.ai/code/session_01Xxpwzvt4ucaGfbeCr77Jns

@sanity sanity left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comprehensive PR Review: #212 (round 4 re-review, plus a light re-check)

  • HEAD SHA: 119aa16. CI is green on it: ghostkey-verify test and msrv, Rust API Tests, Freenet Deploy build.
  • Tier: Full.
  • Round 4 ran at 8ccd956: freenet:code-first-reviewer, freenet:testing-reviewer, freenet:skeptical-reviewer, freenet:big-picture-reviewer, and the contract-safety / inbox-parity lens.
  • Light re-check: an independent freenet:skeptical-reviewer read the delta 8ccd956..119aa16. That delta is tests, comments and dependabot.yml only; no library code changed.
  • External model: codex review was unavailable (usage limit) at every attempt this session. The extra Claude lens substituted for it in each round, per policy, without waiting.

Round 4 result: no Must Fix

  • Parity evidence. The contract lens ran about 221k inputs, natively and under wasm32: multi-block texts mixing labels, decoys failing in 16 different ways, END and body variants, and the production certificate among decoys. The skeptical lens ran about 290k structural and text mutations. Neither found an input where this crate's verdict, chosen certificate or canonical form differs from gklib (run through the inbox's own steps), apart from the documented refusals. Every canonical form re-parses.
  • Mutation testing. Every round-3 guard is caught. The only survivors are equivalent mutants: armour lengths that cannot occur, and checks duplicated by rsa.
  • cargo publish --dry-run. Clean. The 0.1.0 API and docs are judged right to publish.

Addressed after round 4 (119aa16, tests and comments only)

Finding Severity Resolution
The two-block guard failed only through the single-block assertion Should Fix (testing) Split into two tests; removing the early return fails each by name
No wasm vector reached the canonical-size refusal Consider Added oversized-canonical-form (asserted to fit as sent); 19 vectors pass under wasm
armor.rs comment overclaimed that gklib's chain then fails Consider Reworded: the inbox's canonical-size check refuses it
Parity holds per resolved dependency set Consider Stated in the crate docs
Dependabot comment overclaimed what ignoring rsa does Consider Softened

Not changed

  • No Dependabot entry for wasm-probe. It is CI-only and never published.
  • Generated Cargo.lock in the package. This is cargo's default for a library and harmless.

Verdict

State: Ready to Merge, on review grounds.
HEAD SHA reviewed: 119aa16. Round 4 was a full re-review at 8ccd956; the later commits passed a light re-check.

[AI-assisted - Claude]

@sanity
sanity merged commit a36563b into main Oct 28, 2026
6 checks passed
@sanity
sanity deleted the feat/ghostkey-verify branch October 28, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant