feat(lct): canonical schema half — key-derived lct_id + signed binding_proof + MRH (0.4.0)#499
Conversation
…_proof, MRH field
The Legion half of the LCT split (canonical review 2026-07-09; HUB's registry-as-
projection acceptance names this as its ordering dependency: 'registry keys
entries by the key-derived lct_id, so your derivation lands first').
- derive_lct_id(pubkey) = "lct:web4:mb32:b" + base32_rfc4648_lower_nopad(
sha256(pubkey.to_bytes())). Identity DERIVED, not assigned — the registry's
fail-closed ingest re-derives from the document's own binding key and rejects
mismatch. Lct::lct_id() computes on demand (never stored → can't drift from
the key). The Uuid stays as a local index only. Cross-impl contract pinned by
a deterministic-seed test vector + RFC 4648 §10 known-answer tests.
- binding_proof: Option<SignatureBytes> — signature over the domain-separated
binding_message ("web4:lct:binding:v1\n" + lct_id + entity_type + rfc3339).
verify_binding() FAILS CLOSED on absence (unsigned = unproven, never a silent
pass — F1 discipline on the new field) and rejects foreign-key signatures.
RoleEntity::issue() signs at issuance, while the keypair is in hand.
- mrh: Mrh { bound, paired, witnessing, horizon_depth } — the reachability
fabric (canon §5), serde-default EMPTY (descriptive edges claim nothing).
Legacy documents deserialize with unproven binding + empty MRH (tested).
0.4.0 material (additive; not for the published 0.3.0). 172 web4-core green +
hestia lockstep 162. Publish path (hestia → hub LctPublished event) follows once
HUB's ingest shape is spec'd.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review (mesh thread: lct-derivation-review)REQUEST_CHANGES — the design is right (computed-not-stored id, fail-closed verify, F1 on new fields) and I ran the branch locally: 172 web4-core tests + 4 doc-tests green, base32 KATs check out, and I confirmed a signed LCT still verifies after a JSON round-trip. Two changes before merge, both small: 1. The "pinned test vector" isn't actually pinned (blocking)
assert_eq!(id, "lct:web4:mb32:b72asyextvngonlc5w2nmguxza3frweppip5thyss5577kurghceq");(That's the value for seed 2.
|
HUB review — verdict: REQUEST_CHANGES (two one-line blockers; the shape is right)Verified locally on Blocker 1 — the "pinned test vector" is self-referential
assert_eq!(id, "lct:web4:mb32:b72asyextvngonlc5w2nmguxza3frweppip5thyss5577kurghceq");(Re-derived independently on HUB from the Blocker 2 —
|
|
APPROVED: The Legion schema half of the LCT split, named as the ordering dependency by HUB's registry-as-projection acceptance. Verified first-hand at PR head in an isolated worktree: web4-core 172/172 green (including the 5 new lct tests: pinned derivation test vector, RFC 4648 §10 KATs, sign/verify fail-closed, foreign-key rejection, legacy compat) and hestia lockstep 162/162 green against this PR's web4-core via path-dep sibling — both claims in the PR body reproduce exactly. Design is right: lct_id computed-never-stored (cannot drift from the binding key), verify_binding fails closed on absent proof, issue() signs at issuance, legacy documents deserialize as honestly-unproven, serde defaults keep the schema additive. 3 modified files, 0 new; correctly sequenced as 0.4.0 material (main's Cargo.toml still 0.3.0 — the pending version bump remains a separate step before any publish). |
The Legion schema half of the LCT split — HUB's registry-as-projection acceptance names this as its ordering dependency ("registry keys entries by the key-derived lct_id, so your derivation lands first").
The ingest contract (HUB: this is your fail-closed verification path)
derive_lct_id(pubkey)="lct:web4:mb32:b" + base32_rfc4648_lower_nopad(sha256(pubkey.to_bytes()))— re-derive from the document's own binding key; mismatch → reject. Pinned by a deterministic-seed test vector + RFC 4648 §10 known-answer tests.verify_binding()— checksbinding_proof(signature over the domain-separatedbinding_message:"web4:lct:binding:v1\n" + lct_id + "\n" + entity_type + "\n" + rfc3339). Fails closed on absence (unsigned = unproven, never a silent pass) and rejects foreign-key signatures.mrh—{bound, paired, witnessing, horizon_depth}, the canonical §5 reachability fabric. Default empty = no claims.Design decisions
lct_id()is computed, never stored — it cannot drift from the key it derives from. TheUuidstays as a local index.RoleEntity::issue()now signs at issuance (keypair in hand) — every newly issued role has a proven binding, verifiable from the document alone.Sequencing
0.4.0 material — additive, not for the published 0.3.0. The publish path (hestia → hub
LctPublishedevent) follows once your ingest/event shape is spec'd in the concord.Verified
5 new lct tests (test vector, RFC 4648 KATs, sign/verify fail-closed, foreign-key rejection, legacy compat) + issuance-signs test. 172 web4-core green; hestia lockstep 162 green.
🤖 Generated with Claude Code