Skip to content

Origin-quorum rule: a log does not vote in its own witness quorum (4.0.0-labeled) - #144

Merged
b7n0de merged 7 commits into
mainfrom
feat/origin-quorum-and-witness-keys
Aug 18, 2026
Merged

Origin-quorum rule: a log does not vote in its own witness quorum (4.0.0-labeled)#144
b7n0de merged 7 commits into
mainfrom
feat/origin-quorum-and-witness-keys

Conversation

@b7n0de

@b7n0de b7n0de commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Origin-quorum rule — a log does not vote in its own witness quorum

Lands the origin-quorum verifier rule (6 commits, origin/main..ca3f2e9). A checkpoint's
witness_quorum no longer counts a signature whose witness is keyed under the origin name of the
log being verified
— a log cannot supply independent-witness weight to itself.

Semantics: changed, in one deliberate, fail-closed direction. Per this project's own
COMPATIBILITY.md (a change that makes previously-valid input invalid is breaking), the version label
for the eventual release is 4.0.0 (MAJOR) — Owner decision 2026-08-17. Every previously shipped
fixture keeps its verdict bit-identically; only rosters that key a witness under the verified log's
origin flip. API is fully additive (origin_excluded + detail added, nothing removed).

Hardening (deep-gate, 3 rounds — fix the class, not the instance)

  • e69ac5b the rule + two operator ML-DSA-44 keys + 8-of-11 fixture coverage
  • 18eaf0a name-only rule was bypassable → key-material exclusion + zero-width origin guard
  • d1a7021 re-gate neighbour: a cloaked witness name (NBSP/zero-width) bypassed the name test
  • c74f58e close the origin-self-cloaking class — positive rule: printable-ASCII identities, not per-category patches
  • ca3f2e9 finish the class — the third identity slot (log key name), the fail-open, the neighbour

Deep-gate run: audit_artifacts/380/ (pre-registration + F1–F7 falsification). Rule test:
tests/test_origin_quorum_rule.py. Full suite green without touching any existing fixture.

Scope boundary (No-Overclaim)

This PR is the merge to main only. The release (tag v4.0.0 + PyPI + Zenodo) is a separate
Owner-GO following RELEASE.md — not part of this PR.

kraxo and others added 7 commits August 17, 2026 11:53
…of-11 fixture coverage

A log never votes in its own quorum. checkpoint.witness_quorum now excludes any
witness vkey whose name equals the checked note's own origin line - fail-closed,
algorithm-agnostic, before any signature math; the excluded entry stays visible
(ok=false, origin_excluded=true, detail). All three public surfaces inherit it
through the shared helper (verify_witnessed_checkpoint, verify_tlog_proof,
public_transparency). Measured 2026-08-16: a self-cosigned mini-log satisfied
threshold=1 with nothing but its own signature; decided with the log operator in
issue #7 - his /policy declares 'group independent-witnesses 4' with the log not
a member, and the C2SP specs are silent, so the verifier holds the line. The
name comparison is exact, held by the shared near-miss corpus.

The markovian_log/proof_7271 fixture now verifies 8 of its 11 signature lines:
the two ML-DSA-44 witness cosignatures are covered by operator-published keys
fetched from outside the audited log (transparency.dev/witnesses for
ring-any-bells 5774b075, the navigli operator page for 6bc44249), each keyid
recomputed and each signature verified over the subtree/v1 message against the
frozen checkpoint with bit-flip counter-probes in signature and root, before
being carried. Without the [pq] extra the same two lines report the missing
backend and 6 of 11 verify - asserted on a real cryptography-44 environment,
not skipped into a green run. The 2026-08-14 note that navigli's page "did not
serve the key" is corrected: the page serves vkeys HTML-entity-encoded and the
old extraction pattern missed that form.

The operator's live checkpoint at tree size 7397, self-signed under the origin
name in both Ed25519 (note signature) and ML-DSA-44 (cosignature shape), is
vendored digest-pinned as the rule's test vector - offered by him for exactly
this purpose. Its Ed25519 half is excluded by the existing 0x01/0x04 domain
separation, its ML-DSA half by the new rule; the carried foreign witnesses
still meet the 4-of-n quorum on it.

ruff clean, mypy clean, unittest discover: 2189 tests OK (skipped=123) with
[pq]; the rule and fixture modules also green on a no-pq cryptography-44 env
(15 OK skipped=1 / 26 OK). Mutation gate runs in CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…exclusion + zero-width origin guard

Deep-gate re-gate (2026-08-17) refuted the first cut of the origin-quorum rule:
a name-only exclusion is not enough. Two operands the log does not choose now
gate the count, in witness_quorum and inherited by all three surfaces.

F-2 (HIGH): the Ed25519 cosignature/v1 signed message does NOT bind the cosigner
name (only ML-DSA-44 does), so an origin-named line can be relabelled under any
alias without the private key, and a log reusing its signing key as a cosign key
counted as a witness under any name. witness_quorum now takes log_key_material
and excludes a cosignature whose public bytes equal the log's signing key —
algorithm-agnostic, independent of the claimed name. verify_witnessed_checkpoint,
verify_tlog_proof and public_transparency all pass it.

F-1 (CRITICAL): a zero-width / format character in the origin line made it
byte-different from a witness name while looking identical, defeating the exact
name compare ('​'.isspace() is False, so the old guard missed it).
_origin_has_invisible rejects the Cf class at verify time; narrow on purpose so
visible-space origins (Go sumdb) and control chars (terminal neutralisation is
tested separately) keep verifying.

F-4 (MEDIUM): verify-proof --json now carries origin_excluded and detail per
witness, so an excluded witness and a bad-signature one are no longer
byte-identical output (the reason existed in the library, dropped before output).

Honest limit, now documented at the call sites and in SPEC/CHANGELOG: a log
cosigning with a SEPARATE key under a non-origin alias that a relying party
wrongly trusts as an independent witness is roster provenance, not a local check.
The name test stays exact and is defence-in-depth (robust for ML-DSA, whose
message binds the name — Colin's live vector).

tests/test_origin_quorum_rule.py: TestOriginQuorumHardening pins F-1/F-2/F-4 on
the public surfaces plus the exact-relabel case; the meta-probe catches all four
defect axes (name loosened/removed, material removed, invisible-guard removed).
ruff clean, mypy clean, unittest discover 2196 OK (skipped=123); the four Go
sumdb / Rekor external vectors and the terminal-neutralisation test stay green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Owner 2026-08-17)

The external audit is decoupled from the version line: it happens when maturity
is reached, whatever version is current then. Version numbers are pure
COMPATIBILITY.md labels; no version means audited. The four places that coupled
"4.0.0" to the audited/stable state are made version-free.

- PROGRESS.md: the denominator is distance to "the externally audited, trustworthy
  state", defined by audit completion and closed findings, not a version.
- PROGRESS.md: the irreducible ceiling is "outside the audit scope", not "outside 4.0.0".
- AUDITOR_OPEN_POINTS.md §F: the trustworthy stable state = pack + audit + findings
  closed + wire-freeze, defined by those facts and carrying no version label.
- claims_hygiene_check.py: the comment quoting the old goal is updated to the
  version-free wording (and states the decoupling).

Repo-wide sweep for the coupling: the only "4.0.0"↔audit occurrences were these
four (grep in report). "audit-candidate" (a maturity label = not yet audited) and
"3.6.0" (a historical release line) are NOT couplings and are left as-is — the
Owner decision entangles version with audit STATUS, which these do not.

readiness-pack MANIFEST.sha256 + advisory self-receipt regenerated for the two
edited pack files (scripts/readiness_pack_manifest.py --generate); claims-hygiene
PASS, audit-candidate manifest test green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dth) bypassed the name test

The un-review of the F-1/F-2 fix found the mirror of F-1 on the other operand:
the cloak can sit in the witness NAME, not only the origin line. A cosignature
under a name like "markovianprotocol.com/log " (NBSP, a visible-looking
space) or with a zero-width char parses and verifies on the verify path, looks
identical to the origin, yet is byte-different — so the exact name compare misses
it and an ML-DSA self-cosignature (where the key-material test does not apply,
because the ML-DSA key is not the log's Ed25519 signing key) counts in the log's
own quorum. Reproduced end to end before fixing.

Root cause and fix: the emit path (cosign_checkpoint / _mldsa) always rejected a
whitespace witness name; the VERIFY path (_parse_witness_vkey) did not. It now
rejects any whitespace or zero-width/format character in a witness name — the
same rule, now on both paths. The origin-line guard is widened the same way
(_name_has_invisible): any non-plain-space whitespace (NBSP, ideographic space,
tab, CR/LF) or Cf character is refused, while the single plain ASCII space stays
legal so Go sumdb's "go.sum database tree" origin keeps verifying — and a spaced
origin can never equal a (space-free) witness name anyway.

un-review round 2 also raised a non-str witness vkey as a possible bypass; I
reproduced it and it is NOT one — a non-str vkey raises BundleFormatError
(fail-closed), the documented contract, so it never counts. Recorded, not acted
on (the input is part of the review).

test_origin_quorum_rule: NBSP/zero-width/ideographic/tab witness names rejected
end-to-end; the plain-space origin still verifies and is never excluded; the
near-miss corpus test now treats a malformed near-miss as "not the origin". The
meta-probe catches five defect axes now (name loosened/removed, material removed,
invisible-guard removed, witness-name-guard removed). ruff clean, mypy clean,
unittest discover 2199 OK (skipped=123); external vectors + terminal test green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dentities, not per-category patches

Adversarial re-gate (opus lens) refuted the name-only fix again, and named the
class instead of one more instance: the exclusion compared a verifier-side
constant against an operand the AUDITED party writes (its own origin line), and
each round patched one character class (Cf → NBSP/Zs → variation-selector/
Default-Ignorable → an appended plain ASCII space) while the class stayed open.
F-5 (HIGH): a single trailing space on the log's own origin, or a Hangul filler
(Lo, Default_Ignorable), made origin != the clean witness name the roster lists,
and the ML-DSA self-cosignature (key-material test does not apply — separate
key) counted. Reproduced all four variants end to end.

Durable, non-enumerated fix: an origin must be PRINTABLE ASCII with no
leading/trailing or double space (one internal space stays legal for Go sumdb's
`go.sum database tree`); a witness name must be printable ASCII with no space at
all. None of the cloaking characters is printable ASCII, so the whole look-alike
class is closed in one rule instead of chasing Unicode categories forever. The
key-material exclusion remains the robust, algorithm-agnostic prong; the name
prong is now a byte-exact compare between two guaranteed-clean identities.
Deliberate, documented restriction: a non-ASCII/IDN origin is refused for the
identity compare (no real tlog origin is non-ASCII) — which also closes the
NFC/NFD normalisation question at the root.

Also from the re-gate:
- F-6 (MEDIUM): the human text path of verify-proof now prints an indented
  reason line per non-verifying witness with a detail — the exclusion reason
  existed only in the library and in --json; the reader most likely to act saw
  nothing.
- F-7 (MEDIUM): `_log_key_material_of` now catches ValueError/TypeError, not just
  BundleFormatError, so a lone-surrogate log-vkey name (UnicodeEncodeError) can no
  longer escape as a raw traceback through the new public_transparency call site.
- F-8 (MEDIUM): documented honest limit — public_transparency applies the
  key-material prong only when a log_vkey is supplied (optional there, EXPERIMENTAL
  profile); the always-wired surfaces always pass it.
- F-9 (LOW): fixed by the printable-ASCII builder rule (control chars refused).

Tests: test_f5_origin_self_cloaking_is_the_whole_class pins appended-space, Hangul
filler, variation selector, combining mark, zero-width, NBSP and non-ASCII — all
refused; the NFC/NFD class becomes "non-ASCII origins are malformed"; the terminal
test now asserts a control-char origin is rejected fail-closed (stronger than
neutralised); the meta-probe catches five defect axes, all syntactically clean.
ruff clean, mypy clean, unittest discover 2196 OK (skipped=123); Go sumdb / Rekor
/ Colin's fixtures + the terminal-neutralisation test all green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…), the fail-open, the neighbour

Third adversarial re-gate: the printable-ASCII rule closed origin and witness
name but the class was still open — the classic fix-the-instance failure. Three
real neighbours, all reproduced end to end before fixing.

F-8/F-10 (log key name — the third identity slot): key_id, cosign_key_id,
cosign_key_id_mldsa, sign_checkpoint and _parse_vkey now all require a
printable-ASCII name (no space, no '+') BEFORE name.encode("utf-8"). A surrogate
log key name therefore raises a typed BundleFormatError instead of a raw
UnicodeEncodeError out of the public verify API (verify_checkpoint /
verify_witnessed_checkpoint are in __all__), and a zero-width log key name can no
longer substitute for a real one in a note.

F-9 (the F-7 never-raise fix had turned a loud crash into a SILENT fail-open):
public_transparency's witnessQuorum path had two states (material present / no
context = None). A supplied-but-malformed log_vkey gave _log_key_material_of=None,
which silently switched the key-material exclusion off and let the log vote in its
own quorum under an alias with errors=[]. Now THREE states: a log_vkey supplied
but unusable is "not measurable" and fails closed with a named error; absent
log_vkey still passes (documented limit); well-formed still excludes via the
material prong.

F-8 neighbour, different module: anchors_rootcommit.build_preimage encodes
attacker-controlled checkpoint fields; verify_rootcommit_v1/v2sig now catch
(UnicodeError, ValueError, TypeError) and return malformed_checkpoint, honouring
that module's own "never a raw exception" contract.

F-11 (honesty): the remaining limit is named in full — the byte-exact name compare
does NOT catch byte-different forms of the SAME identity (ASCII case variant,
FQDN trailing dot, path-normalisation); those are the same owner, not a look-alike,
and the robust defences are the key-material prong and expected_origin. Exactness
is kept deliberately (normalising would loosen expected_origin, whose safe
direction is the reverse).

Tests: TestAllThreeIdentitySlotsAreHardened (surrogate never raises UnicodeError,
zero-width keyname rejected, no trust substitution) + TestPublicTransparencyFailsClosedOnUnusableLogVkey.
ruff clean, mypy clean, unittest discover 2202 OK (skipped=123). un-review round 4
ACCEPT on all four questions. Deep-gate history across four iterations, each fixing
the neighbour the previous one left, is in the return report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n a K-origin

The mutation gate flagged 'tlogproof: origin comparison normalises canonically (NFC)' as SURVIVED
(UNEXPECTED). Falsification-verified root cause: the frozen fixture origin markovianprotocol.com/log
is pure ASCII and NFC is the identity on ASCII, so no --expected-origin against it can distinguish an
NFC-normalising compare from an exact one. OriginVergleichIstExakt kills the NFKC sibling (full-width
-> ASCII is a compatibility mapping) but NFC needs an origin the fixture cannot provide: one of the
three Unicode->ASCII NFC singletons (';' '`' 'K'), none of which is in the fixture origin.

Adds a self-signed checkpoint with 'K' in the origin (printable-ASCII, accepted by the origin rule)
and pins the KELVIN SIGN U+212A near-hit (NFC(kelvin)=='Klog...'): exact rejects, NFC accepts.
Verified against the planted operator line: log_ok flips False->True under it, so the test is red on
the mutant and green on real code. The operator stays should-kill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@b7n0de
b7n0de merged commit 391eb37 into main Aug 18, 2026
22 checks passed
@b7n0de
b7n0de deleted the feat/origin-quorum-and-witness-keys branch August 18, 2026 01:00
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