Human Identity in ICN — a first-principles architecture
What a human identity should be in ICN, how cryptographic authority over it should evolve, what institutions and peers should observe, and how devices act for humans.
Truth status, section by section. No section is left unclassified; §20 is the full per-result index.
Sections Status Meaning §1 summary restates results established or recommended below; carries no independent authority §2 normative requirements adopted for this analysis, each labelled HARD / STRONG / SOFT. They are chosen, not discovered — a reader may reject one, and §7's verdict may move if they do §3 descriptive verified at b26bf681with file:line citations. Load-bearing rows re-read by hand; §3.0 marks which§4 ESTABLISHED — derivation over §3 the decomposition, and the proof that a durable subject identifier cannot be a public key. It follows from §2's requirements plus §3's facts and holds unless one of those is rejected §5 ESTABLISHED as a diagnosis; REJECTED as a verdict on Model A that the six blockers share one cause is a derivation over §3, checkable by re-tracing each row. The consequent rejection of key-derived durable identifiers is a design decision, recorded REJECTED in §20 §6 mixed, per claim §6.1 is RECOMMENDED on a bounded argument (not ESTABLISHED — see the caveat there); §6.2–§6.3 are ESTABLISHED external results; §6.4–§6.5 are summaries §7 onward PROPOSED architecture design, not implementation No claim here asserts production operation, pilot readiness, or institutional adoption. Prose that reads like a fact but is not classified ESTABLISHED is a defect in this document.
Relationship to PRINCIPAL_MODEL.md. That document's §1–§3 (the invariant, the verified current state, the classification of historical intent) remain the best evidence map in the repo and are retained; §3.0 below lists the corrections this pass found in them. Its §4–§16 (the proposed four-class taxonomy, member-origin signing via a carried
DeviceAuthorization, and the A0→A→B→D slice program) are superseded by this document — not because they were careless, but because the open decisions they accumulated (O8, O9, O13, O16, O17, O18) turned out to be six symptoms of one structural choice, which §5 identifies and §6 removes.
Adversarial review, and what it changed. This document was attacked on ten axes before publication (hidden clocks, hidden arrival-order dependence, hidden correlation, hidden permanent root, hidden registry, claims-vs-source, proposed-as-current, invented protocol, internal contradiction, unmet HARD requirements). It found nine blocking errors, all of which are corrected in place with the correction marked rather than silently removed. The four that most changed the result: (1) pre-rotation constrains rotations only, so a compromised current key can still fork a non-establishment event (§6.2); (2) the impossibility claim was overstated and is downgraded from ESTABLISHED to a bounded argument (§6.1); (3) under the default recovery path, continuity-root compromise is a takeover and is unrecoverable (§12.2); (4) the class-1 fallback is a known R5.1 violation, not a safe default (§9.3).
Round 2 attacked the replicated state model specifically and found seven more blockers, all corrected: (5) the durable element must be a canonical body, not an event — Ed25519 admits several valid signatures per message, so an event-keyed set is really a map with an undefined value merge (§9.2.1); (6)
σandprev_digestmust use the body digest too, or a re-signing mints a secondSubjectIdand a child can name a discarded parent (§9.2.1); (7) pre-rotation commits to the next key set, not the body, so rotation bodies must be deterministic or an honest crash-retry halts the subject permanently (§9.2.1); (8) independent M-of-N guardian signatures give anyMguardians a kill switch, making threshold signing a prerequisite rather than an upgrade (§12.2); (9) a single compromised current key halts a subject unilaterally, and superseding recovery orphans the entire suffix (§9.2.1, §9.2.2); (10) class-2 settlement is not convergent under a fork —Live(A) → Live(B)leaves permanently different settled state, so this model is not by itself a sufficient basis for irreversible settlement (O-N7); (11) the durable set is attacker-writable, so eviction must be protocol-specified or convergence-under-attack is vacuous (O-N8).Round 3 found two more: (12) a position bound does not stop spam at
frontier + 1from evicting the legitimate next event — eviction must be authority-aware, and the gap region beyond the frontier remains unsolved, so R4.6 and permutation-invariant convergence hold only absent adversarial storage pressure (§9.2.1, O-N8); (13) an existingdid:icn:<key>cannot serve as aSubjectId, because inception admission requiresσ == event_id(b)— migration must allocate a new subject naming that DID as its initial key, plus a membership bridge (§15.1).Round 4 refuted two more claims: (14) the position validity window bounded nothing as specified — the signer chooses the position an act references, so a compromised device keeps referencing its original position forever. It binds only against an evaluation position the signer does not control, which exists for class 1 (once O-N6 lands) and not at all for class 2 (§9.3); (15) superseding recovery orphans historical authorization evidence, so R2.3 is qualified: receipts from orphaned positions keep a verifying signature but degrade from "authorized" to "authored on a superseded branch" (O-N9).
Round 5 found three more: (16) FROST alone does not remove the guardian kill switch — a group key authenticates a quorum but does not bind it to one message, so closing it needs enforced canonical body derivation plus one next-authority commitment per position (§9.2.1 constraint 3); (17) §9.4's normative verification flow still checked the signer-chosen position after round 4 fixed only the narrative (§9.4 step 4); (18) deriving the initial device key from
SubjectIdis a fixed point —S = H(inception(KDF(secret, S), …))— so genesis is uncomputable; the initial key must derive from a pre-subject context nonce (§11.1).§9.6 lists every shortfall the model does not discharge.
Related, deferred to, not restated: authenticated-governance-replication (#2469 — the convergence doctrine this design must satisfy) · AUTHORITY_SPINE (attenuation / revocation lifecycle) · MEMBER_STANDING (the derived read model) · KERNEL_APP_SEPARATION (the Meaning Firewall) · passport-keyring-position-receipt (vocabulary doctrine)
1. The executive answer
A human identity in ICN should not be a globally published identifier at all.
It should be three separable things, and ICN's current difficulty comes almost entirely from having collapsed them into one 32-byte string:
| Layer | What it is | Who sees it | Lifetime |
|---|---|---|---|
| Cryptographic principal | a key. Exactly what Did already is. |
whoever verifies a signature | as long as the key is held |
| Context subject | the identifier a person is known by inside one institution | that institution's members | as long as the relationship |
| Continuity root | a private secret the person's own app holds, linking their contexts and driving recovery | nobody but the person | lifetime, recoverable |
The person is not any one of these. The person is the human who holds the continuity root and, through it, organizes a set of context relationships, each of which is currently exercised by one or more device keys.
Three consequences follow immediately, and they are the substance of this document:
Didshould mean exactly one thing — a cryptographic principal (a public key). Every other use of it today (privacy anchor, treasury identifier, stable subject across rotation, credential subject) is type reuse that the type cannot support, and §3 shows it failing in production code.Authority to act for a subject evolves in a log the subject alone writes, and each institution decides for itself how fresh that log must be. The subject keeps a single-writer key-event log; the institution is the relying party that holds a copy and sets its own recency bar. Neither owns the other: the person cannot force an institution to accept them, and the institution cannot rewrite who the person is. #2469 §9 already reached the neighbouring conclusion for key rotation ("v1 treats rotation and revocation as membership changes"); §6 shows what it generalizes to. That generalization dissolves two blockers (O8, O18), partly answers two (O13, O17), narrows one (O16), and rejects one as posed (O9) — see §6.5, which is the authoritative summary.
There is no convergent global device revocation, and there cannot be one without paying a price ICN has declined to pay. Agreeing on a time-varying predicate across an asynchronous partition-tolerant network requires a synchrony assumption, randomization, or an ordering authority — ICN rejects all three (§6.1). The correct move is to stop asking. Revocation must be scoped to a relying party that has an authoritative view — which is why ICN's one revocation mechanism that is designed this way (RPC/gateway token revocation, scoped to a single gateway with a durable store and a fail-closed read, AUTHORITY_SPINE §2) has a coherent story where identity revocation does not. (That is an architectural observation about its shape, not a re-verification of its runtime behaviour this pass.)
The recommended architecture is Model C of §7 — context-scoped identity with a private continuity root — and §9 states it precisely.
What this does not do. It does not eliminate hard work; it relocates it. The recommendation depends on a per-subject, single-writer key-event log that does not exist today (§3.7 proves the absence of any ordering substrate). That is one named, bounded piece of infrastructure whose cost is formally understood — a single-writer object has consensus number 1 and is implementable with reliable broadcast alone (§6.1) — replacing six unbounded open questions about a global namespace. That trade is the whole argument, and §9.6 states honestly what remains open.
2. Normative requirements
Derived before any representation was chosen, from the mission brief, the Constitutional Core immutable principles, #2469's invariants I1–I12, and PRINCIPAL_MODEL §1's five invariant tests.
Labels: [HARD] a violation is a defect · [STRONG] tradeable only against another HARD/STRONG, explicitly · [SOFT] a goal whose cost may exceed benefit.
R1 Sovereignty
- R1.1 [HARD] Genesis requires no registrar, no network, no permission.
- R1.2 [HARD] No institution, node, gateway or operator holds a key that lets it author acts as a human. Custody defines identity.
- R1.3 [HARD] Loss of any infrastructure must not destroy the human's identity or their ability to establish identity elsewhere.
- R1.4 [STRONG] No single institution may revoke a person's identity. It may revoke standing within itself only.
R2 Continuity — three properties that must not be conflated
- R2.1 [HARD] Continuity of subject. The same human remains the same subject across device replacement, key rotation, total device loss, algorithm change.
- R2.2 [HARD] Continuity of authority. Which keys may currently act is time-varying and must change without changing the subject.
- R2.3 [HARD] Validity of history. An act validly authored when made stays
verifiable as having been made, forever, independent of later authority
changes. (Verifiable ≠ still-effective — see R5.)
Not fully met by the recommended model. Superseding recovery orphans the suffix, so receipts from positions after a fork retain a verifying signature but lose their authorization proof (§9.2.1, O-N9). R2.3 holds unconditionally only for positions preceding the earliest fork.
- R2.4 [STRONG] Subject continuity must not require a permanent master secret whose compromise is unrecoverable.
R3 Multi-device and least privilege
- R3.1 [HARD] A device holds its own key and never receives another principal's key.
- R3.2 [HARD] Delegated authority is a strict subset of the delegator's own (attenuation). Currently violated — #2590.
- R3.3 [HARD] Every security-relevant field of a delegation is cryptographically bound to it. Currently violated — #2588.
- R3.4 [STRONG] Adding or replacing a device must not require the highest authority to be online, except where root-only is a deliberate choice.
- R3.5 [STRONG] Device classes differ by scope and custody strength, never by principal class.
R4 Determinism and partition tolerance (inherited from #2469; non-negotiable)
- R4.1 [HARD] No validity decision depends on the receiver's wall clock.
- R4.2 [HARD] No validity decision depends on the order in which a receiver learned two independent facts. (I12)
- R4.3 [HARD] Two honest nodes with the same durable state reach the same verdict. (I10)
- R4.4 [HARD] Verification never fetches authority material over the channel carrying the act. (I9)
- R4.5 [HARD] Authority may be evaluated against receiver-local replicated
state iff that state is itself authenticated and converges deterministically.
This corrects PRINCIPAL_MODEL §6.2's "carried, not resolved", which over-generalized #2469 §14.1. #2469 resolves against local durable state at ingress steps 7–11 by design; what it forbids is resolving against unauthenticated state (§7.0: "the receiver would compare it against commons state that is itself unauthenticated"). The distinction is load-bearing: it is what makes §9 possible at all.
- R4.6 [HARD] A legitimate act is eventually applied; nothing is permanently discarded because of arrival order. (I11/I12)
R5 Revocation — a requirement on effect, not on admission
- R5.1 [HARD] A compromised device loses the ability to produce new effective acts within a bounded, stated window, without requiring a global total order.
- R5.2 [HARD] Revocation semantics is stated per act class and never differs silently between classes.
- R5.3 [STRONG] Revocation does not retroactively invalidate acts whose effects have already settled irreversibly. Without rollback semantics that is a correctness bug, not a security win (PRINCIPAL_MODEL §6.5 established this).
- R5.4 [HARD] "Could not determine whether this was revoked" never resolves to "not revoked". (AUTHORITY_SPINE §2)
R6 Privacy — structural
- R6.1 [STRONG] Two institutions cannot correlate a person by default from protocol-visible identifiers alone.
- R6.2 [STRONG] A person proves standing in a context without revealing other contexts.
- R6.3 [STRONG] A person may, at their option, prove two personas are one human.
- R6.4 [STRONG] Recovery does not require deanonymizing every persona.
- R6.5 [SOFT] Network- and device-layer correlation is outside identity's sole control, but must not be made worse.
- R6.6 [HARD] Sybil resistance must not require a public global person identifier. Uniqueness ≠ identification.
R7 Offline and partitioned operation
- R7.1 [HARD] Genesis works fully offline. R7.2 [HARD] Signing works fully offline. R7.3 [HARD] Delayed/reordered delivery is safe and converges.
- R7.4 [STRONG] Multiple devices may act concurrently while partitioned.
R8 Authorship and governance
- R8.1 [HARD] Infrastructure never manufactures human authorship.
- R8.2 [HARD] These five stay distinct: session authentication · content authorship · delegated authority · institutional authority · transport identity.
- R8.3 [HARD] A receiver establishes authorship from bytes in hand plus authenticated local state — never from unauthenticated state.
- R8.4 [STRONG] A relay relays without holding any key of the author.
R9 Recovery
- R9.1 [HARD] Recovery is possible from total device loss.
- R9.2 [HARD] Recovery is an authenticated, auditable event, never a silent substitution.
- R9.3 [HARD] Competing recovery attempts resolve deterministically; the losing attempt is visible.
- R9.4 [HARD] M-of-N counts distinct authenticated participants. (#2591)
- R9.5 [STRONG] Recovery does not create a new permanent single point of catastrophic failure.
- R9.6 [STRONG] Recovery is contestable by the legitimate subject (delay + veto).
R10 Usability
- R10.1 [HARD] A member never needs to understand DIDs, PKI, Merkle trees, quorums or key-event logs to join and participate.
- R10.2 [HARD] Phone-only onboarding produces a secure, recoverable identity by default — not a degraded one to repair later.
- R10.3 [STRONG] At most one irreversible user decision at onboarding.
R11 Infrastructure independence
- R11.1 [HARD] Identity works with no node / one personal node / many personal nodes / hosted-only. Moving between them changes hosting, never identity.
R12 Evolvability
- R12.1 [HARD] Signature algorithm replaceable without "create a new person".
- R12.2 [HARD] New device classes without protocol change.
- R12.3 [STRONG] A migration has an enforceable end state. A dual-stack that never ends is not a migration (#2469 / O13).
R13 Meaning Firewall (constitutional)
- R13.1 [HARD] The kernel holds keys, identifiers and opaque scopes — never
Person,member,role,cooperative.PRINCIPAL_MODEL §1.1 states this, and then §4 proposes
PersonandDeviceas principal classes inicn-identity. That tension is real and §9.5 resolves it.
Invariant tests (PRINCIPAL_MODEL §1, retained verbatim)
T1 replacing a phone must not create a new human · T2 replacing a node must not create a new cooperative · T3 hosting a cooperative must not make the host the cooperative · T4 assigning a personal node to a coop must not rewrite the node's identity · T5 running one institution's workload must not change the node's species.
3. What ICN implements today
Verified at b26bf681. This section is descriptive. It does not restate
PRINCIPAL_MODEL §2, which remains accurate on the points it covers; it records
what this pass found in addition to and in correction of it.
3.0 Corrections to PRINCIPAL_MODEL §2, and new findings
Rows marked verified were re-read in source by hand during this pass; rows marked (agent-traced) were produced by a source-tracing pass and spot-checked rather than fully re-read. One agent-traced claim was checked and found wrong — see F4 — so the (agent-traced) rows should be treated as high-confidence but not yet load-bearing, and re-verified before any of them is used to justify deleting code.
| # | Finding | Evidence | Status vs PRINCIPAL_MODEL |
|---|---|---|---|
| F1 | ≈50% of hash-derived DIDs cannot be deserialized. A SHA-256 digest is a valid compressed Ed25519 point only about half the time; Did's hand-written Deserialize routes through from_str (icn-identity/src/lib.rs:181-189) which calls VerifyingKey::from_bytes. So an anchor-derived Did serializes and stores fine and fails to read back ~50% of the time. Measured 1994/4000 = 49.9% valid over SHA-256 outputs (independent check this session). Corroborated in-tree: icn-governance/src/mandate.rs:404 and apps/governance/src/manager.rs:10309 both warn that fixtures must hand-pick seeds that decompress. |
verified | New. O18 understates this: it is not only "no controllable key", it is a coin-flip serialization defect. |
| F2 | icn-zkp silently substitutes an all-zero issuer key. prover.rs:227-230: match attestation.issuer_did.to_verifying_key() { Ok(vk) => *vk.as_bytes(), Err(_) => [0u8; 32] } with the comment // Fallback for test/development, then proceeds to generate a proof. Combined with F1, an anchor-issued attestation produces a proof bound to a key nobody holds instead of failing. |
verified verbatim | New. |
| F3 | The SDIS anchor is documented as not key-derived, on purpose. anchor.rs:1-13: "Unlike traditional DIDs where the identifier is derived from a public key, an SDIS Anchor is derived from a VUI"; properties listed are Permanent (survives rotation), Recoverable, Unique (sybil resistance), Privacy-preserving. |
verified verbatim | New context for O18 — the anchor is a deliberate durable-subject primitive, and to_did() is what breaks it. |
| F4 | The VUI→anchor derivation is wired — and it is fed a VUI derived from the public DID. Anchor::from_vui is called at icn-commons/src/inner.rs:106, inside create_anchor_from_enrollment (:76), which is production-reachable from icn-gateway/src/api/commons/mod.rs:492, icn-gateway/src/api/sdis/simple_enrollment.rs:703 and icn-core/src/services/sdis_service.rs:643. But the VUI is SHA256("gateway-enrollment-vui:" ‖ did) (inner.rs:81-87), with the in-source comment "Generate pseudo-VUI from DID (in real SDIS, this comes from ceremony)". genesis_random is generated (:90-92) but is not a field of Anchor, so it is discarded and the anchor cannot be re-derived from a re-presented VUI. |
verified by grep + read | New. Together with F6 this is the second independent site where the VUI is a pure function of the public DID, so no deployed anchor carries the uniqueness or unlinkability the design claims. (An earlier draft of this row, from a subagent trace, said from_vui had no production callers. That was wrong and is corrected here.) |
| F5 | Even in the SDIS enrollment path the anchor is already a label, not a principal. simple_enrollment.rs:667: // The DID is the ephemeral_did - in SDIS, keys are created on the device; the token is minted for &ephemeral_did (:782). The anchor is returned alongside as anchor_id. |
verified verbatim | New. Anchor-derived DIDs carry no authority anywhere. |
| F6 | The routed enrollment endpoint bypasses the threshold PRF entirely. compute_temporary_vui(did) = SHA256(did.to_string()) (simple_enrollment.rs:76-80, used :606), a pure function of the public DID. This destroys the VUI's own stated properties — vui.rs:17-19 claims "Private: VUI reveals nothing about the person" and "Unlinkable: Without pepper, can't reverse VUI to identity". The file admits it: :13-16 says "NO sybil resistance". |
(agent-traced) | New. |
| F7 | combine_prf_partials is XOR-then-hash, not a threshold scheme. icn-crypto-pq/src/threshold.rs:103-128. Different t-subsets XOR to different values, so the VUI is reproducible only from the exact set used at derivation — n-of-n in effect, not 3-of-5. The header itself lists "(Future) FROST threshold signatures". |
verified verbatim | New (PRINCIPAL_MODEL §4.1.1 noted "PRF not signing"; the availability consequence is new). |
| F8 | The gateway multi-device subsystem is unreachable in production. IdentityManager::get_or_create_document (icn-gateway/src/identity_mgr.rs:60) has exactly three references repo-wide: the definition and two unit tests (:510, :515, :533). No route, enrollment path or bootstrap ever creates a DidDocument, so register_device always returns NotFound at :158. |
verified by grep | Corrects the gap matrix, which rates "Device keypair + roster" as I — implemented. It is implemented and not reachable. |
| F9 | RotationEvent::verify() and DidDocumentCache::apply_event have zero production callers, and apply_event never calls verify() at all. IDENTITY_UPDATES_TOPIC (sync.rs:23) has no publisher and no subscriber. |
(agent-traced, grep-verified) | Extends PRINCIPAL_MODEL §6.5 row 1: not only unwired, but the apply path would not verify even if wired. |
| F10 | The one production rotation signer and the verifier sign different messages. icnctl signs a format string — format!("{}:add_device:{}:{}:{}", …) (bins/icnctl/src/main.rs:6302-6311) and the revoke analogue (:6409-6418) — while verify() checks a signature over icn_encoding::encode(event_with_proof_cleared) (multi_device.rs:583,594-601). Every rotation event ICN actually produces would be rejected by its own verifier. |
(agent-traced) | PRINCIPAL_MODEL noted this for revoke; it holds for both kinds. |
| F11 | is_anchor_did is a tautology — self.as_str().starts_with("did:icn:") (anchor.rs:203-209), true of every valid Did. |
verified verbatim | Confirms "indistinguishable by format" with the stronger statement that the discriminator function itself is vacuous. |
| F12 | RotationEvent::signing_message is bincode over the struct with no domain separator (multi_device.rs:594-601), sharing a signing domain with any bincode payload of the same shape. |
(agent-traced) | New; folds into O10's obligation. |
| F13 | RPC recovery lets a caller name an arbitrary victim, and the node signs the trustee attestation. handle_recovery_initiate takes old_did, threshold and delay_period from caller params (icn-rpc/src/handler/recovery.rs:52-58); attest builds the attestation with state.own_signer() — the node's own key (:138, :173-183). RecoveryEvent has no trustee registry, so any attestation counts. Reachable with the recovery:write scope. |
verified verbatim | New, and distinct from #2591, which covers sync.rs / recovery.rs verification and dedup. |
| F13a | F13 is currently inert. RecoveryEvent::finalize() (icn-identity/src/recovery.rs:215-225) only sets a status field and a timestamp; no production code consumes RecoveryStatus::Finalized to change any authority, and sync.rs::apply_event (the only path that would clear verification methods) has no production callers (F9). The defect is a latent critical: it becomes a takeover the moment anyone wires application of recovery events. |
verified by grep | Stated precisely so it is neither overstated nor dismissed. |
| F14 | Every wall-clock expiry check in the identity stack fails open. icn_time::current_timestamp_secs() returns 0 on clock error (icn-time/src/timestamp.rs:34-42), so now > expiry is false and nothing has expired. |
verified verbatim | New; strengthens R4.1's case beyond convergence into fail-open. |
| F15 | No pairwise, per-relationship or per-verifier identifier exists anywhere. Not found workspace-wide: Pedersen commitment, nullifier, linkage secret, re-randomization, BBS+, selective disclosure, scoped DID, HKDF-derived identifier. | (agent-traced) | Confirms O3 is unstarted. |
| F16 | ICN has no working identity-privacy primitive. icn-crypto-pq/src/blind.rs:18-20 self-declares "simplified commit-hash based" and unblind stores the blinding factor in the clear (:153-163) — no unlinkability, and no caller outside the crate. icn-zkp's AIRs set their transition constraints to zero (stark/non_revocation.rs:122-123) — in fairness, the file argues soundness rests on boundary assertions plus trace commitment instead, and points to issue #506 for algebraic constraints, so this is a tracked design position, not a hidden hole; but it does mean the transition relation is not algebraically enforced. Alongside it: the accumulator uses a 32-bit test modulus (accumulator.rs:87-90); verify_non_membership only checks the witness is nonzero (:254-259); is_trusted_issuer returns true when the trusted list is empty (verifier.rs:78-80); and default = [] with stark behind a feature flag (Cargo.toml:57-59), so the default build can neither prove nor verify. icn-privacy is network-metadata privacy (onion routing, topic-name encryption), not identity. |
(agent-traced) | New and load-bearing: any architecture depending on ZK selective disclosure is proposing new cryptographic work, not wiring. |
| F17 | AuthorityGrant and Mandate carry no signature field and are not replicated; membership records carry no signature (icn-entity/src/membership.rs:23-48) and merge by last-write-wins on a wall-clock field (icn-entity/src/actor.rs:315-330); memberships are not gossiped at all — only the parent entity record is announced (actor.rs:277-279). |
(agent-traced) | New; constrains what "authenticated local state" can mean today. |
| F18 | The only cryptographic evidence an institution exists is a set of person signatures over its charter — FounderSignature { did, signature, timestamp } (icn-governance/src/charter.rs:388-397). |
(agent-traced) | Supports PRINCIPAL_MODEL §4.1's conclusion with a concrete existing primitive. |
| F19 | The React Native client has no Person-vs-Device distinction at all. wallet.ts:94-120 generates one Ed25519 key per install and the DID is a direct encoding of it (:106); the private key is stored hex and exportable (:102, :110). Reinstalling produces a different person. sdk/typescript registerDevice signs nothing itself — the caller must hand-reproduce the ICN_ADD_DEVICE: string. |
(agent-traced) | Extends §2.9. |
| F20 | /v1/sdis/anchor add-device and rotate-keys are disabled, not merely unauthenticated: both handlers take no arguments and unconditionally return Forbidden, each carrying a doc-comment reading "Disabled until this route can verify a signed current-key transition… Public anchor data is not authority to mutate an identity root" (api/sdis/anchor.rs:268-278, :301-313). The sibling reads — get_anchor, get_rotation_history, list_devices — remain unauthenticated. |
verified verbatim | Live-state note for #2448, which was verified at b34cd3f6: the write half of that issue is no longer reachable as described, while the unauthenticated read half stands. Reported, not acted on. |
3.1 What a Did actually is
pub struct Did(String) (icn-identity/src/lib.rs:177). from_str (:209-244)
requires did:icn: + multibase decode + exactly 32 bytes + a valid Ed25519 point.
from_public_key (:193-196) always emits base58btc; from_str accepts any
multibase, and PartialEq/Hash are derived over the inner String (:174) —
so Did equality is string equality, not key equality.
new_unchecked (:284) bypasses all of it. It has one call site:
from_anchor_id (anchor.rs:194-197). And from_anchor_id's production use has
almost nothing to do with persons — it is a generic "mint a Did from 32
arbitrary bytes" hatch: cooperative treasury identifiers
(icn-coop/src/actor.rs:493,788; apps/membership/src/coop_core/actor.rs:359,523,
from 16 bytes zero-padded to 32), a ZKP holder pseudonym
(icn-zkp/src/prover.rs:257), and placeholders (icn-ledger/src/fx.rs:118,
icn-gateway/src/api/sdis/qr.rs:163).
ESTABLISHED: Did is a key identifier, and every non-key use of it is type
reuse the type cannot support. F1 and F2 are that reuse failing in production
code, not in theory.
3.2 Genesis is already sovereign — and that part is right
KeyPair::generate() (:341) → Did::from_public_key (:191). No registry, no
network. R1.1 and R7.1 are already met and must be preserved.
3.3 The device model is built, unreachable, and unverifiable
F8, F9 and F10 together: multi_device.rs implements a coherent roster and
rotation model; the gateway path that would use it can never find a document; the
verifier has no callers; and the one production signer signs a different message
than the verifier checks. can_sign has zero production callers (confirmed —
docs/status.toml:32 already records this).
Consequence for #2588 and #2590. Both describe real defects in real code. But the gateway route they sit on is currently unreachable (F8), which changes their urgency without changing their validity: they are correctness debts to fix before the path is wired, not live exploited surface. This document does not propose closing them here; it proposes (§19) that they be fixed as part of whatever wires device authority, so the wiring cannot ship the defects.
3.4 Authority is a bearer token over an HMAC, and one mint path proves nothing
Confirmed against source: join_via_invite (api/invites.rs:245-296) takes
req.did from the request body with form validation only, takes no
HttpRequest, and mints a token for it (:280) — #2589 confirmed
line-for-line. The challenge paths do prove key control, but the signed payload
is the raw nonce bytes only (auth.rs:315), with no domain separator and no
binding to the requested coop_id or scopes.
3.5 SDIS held two incompatible models of its own anchor, simultaneously
This is the most important historical finding, and it is a genuine unreconciled split rather than a decision that was made and lost:
- Anchor as public identifier — it becomes the DID
(
SDIS_IMPLEMENTATION_PLAN.md:344-345), is a URL path key (/v1/sdis/anchor/{anchor_id}), is written to replicated state, is carried in cleartext gossip, is typed by users during recovery, and is returned in an HTTP response alongside its own hex (api/commons/anchor.rs:67). - Anchor as secret witness —
Attestationis "Private attestation from an issuer (kept secret by holder)" (icn-zkp/src/types.rs:217); the membership circuit takes the anchor-derived DID as a private input (icn-zkp/src/circuit/membership.rs:60-63); the audit checklist requires "No VUI/anchor logged" (SDIS_AUDIT_CHECKLIST.md:286).
Anchor-to-anchor correlation across cooperatives is never named as a threat in
any SDIS document. And genesis_random's only documented rationale —
"prevents rainbow table attacks on VUIs" (anchor.rs:102-104) — is a
pre-image defence, which only makes sense if the anchor is published. So the
honest reading is: the anchor was intended to be a publishable pseudonym that
hides its inputs, not a private continuity root — and the ZK layer, built on
the opposite assumption, was never reconciled with it.
Anchor::to_did was in the original SDIS commit (8c434077, 2025-12-10) but was
specified under "Task S2.1.2: Implement DID format extension" with the literal
instruction "Add backward compatibility" — an interop bridge to the
pre-existing key-derived Did, not a doctrine that a person's public identifier
should be their anchor.
3.6 Documented contradictions nobody has adjudicated
The sharpest, which PRINCIPAL_MODEL does not record: does recovery preserve the
DID? design/multi-device-identity-design.md:664 says the DID stays stable;
design/social-recovery-design.md:26,29 says recovery creates a new DID and
installs a did_mapping:<old_did> indirection. Those are incompatible, both are
"PARTIALLY CURRENT", and each has code. Also unadjudicated: whether keys are
exportable (CLIENT_MODEL says never; MDI ships an encrypted BackupSeed), and
whether a person has one DID or many (CLIENT_MODEL:99 vs PRINCIPAL_MODEL:1144).
3.7 There is no ordering substrate. At all.
This is the fact that decides the architecture, so it is stated flatly.
| Substrate | Authenticated | Ordered | Scope | Replicated |
|---|---|---|---|---|
SignedGovernanceOp |
yes (Ed25519 over a length-prefixed canonical body) | no — seq is per-(author, domain) and is explicitly "a comparator for same-key conflicts, never an acceptance gate" (replication.rs:291-294) |
per-domain key, no domain-wide order | no — emission only; ingress recognises the frame and returns without applying (apps/governance/src/actor.rs:1368-1371) |
| Gossip | no — GossipEntry has author: Did and no signature field (types.rs:116-133) |
partial only; the vector clock is node-global, LRU-evicting at 10 000, and never used to order within a topic | topics are global-by-domain (governance:proposal, identity:updates, entity:updates) |
yes |
| Ledger journal | yes (per-entry Ed25519) | partial — Merkle-DAG, local timestamps | per-currency | yes |
| Entity / membership | no | LWW on a wall-clock field | global public topic; memberships not gossiped at all | partial |
| Identity rotation | event is signed, but the apply path never verifies it | per-DID new_version |
per-DID | no — topic unwired |
Coordination / Raft |
— | — | — | zero implementors (icn-kernel-api/src/coord.rs:124) |
ESTABLISHED: ICN has no replicated authenticated ordered log that a governance operation lands on, and no per-cooperative or per-domain order that a governance operation and an identity event could share. The nearest specification of one is #2469 §5.6, and it is unbuilt.
4. What "identity" collapses today
Testing the mission's decomposition hypothesis against §3, Did is currently
serving as eight distinct concepts:
| # | Concept | Where | Must it be a key? |
|---|---|---|---|
| 1 | key identifier | from_public_key |
yes — this is what the type is |
| 2 | durable subject across rotation | DidDocument.id (multi_device.rs:21-22) |
no — see below |
| 3 | privacy anchor | Anchor::to_did |
no, and it must not be public |
| 4 | entity identifier | treasury DIDs from 16 padded bytes | no |
| 5 | network principal | node DID + Hello cert binding | yes |
| 6 | credential subject | icn-zkp holder DID |
no — should be per-context |
| 7 | storage / membership key | sled keys, gov:vote:{pid}:{voter} |
needs canonical bytes, not string-equality |
| 8 | session subject | claims.sub |
no — a session is not authorship |
Result of the decomposition test. Only 1 and 5 genuinely need to be a public key. Concept 2 is the decisive one:
ESTABLISHED: a durable human subject identifier must not be a public key.
Proof. R2.1 requires the subject to survive rotation; R2.2 requires the authorizing keys to change; R1.2 says custody defines identity. If the subject identifier is a key, then either that key can never be retired — contradicting R2.2 — or the subject is permanently named by a key nobody controls. ICN already exhibits the second horn:
DidDocument.idis the original key's DID, so afterrevoke_device("device-1")the document is identified by the key it just revoked (multi_device.rs:21-22,:426-447). ∎
Concepts 2, 3, 6 and 8 must therefore each get their own representation. §9 gives
them one; §10 states what is left for Did.
5. The structural diagnosis: one choice, six blockers
PRINCIPAL_MODEL accumulated O8, O9, O13, O16, O17 and O18 across twenty review rounds. They are not six problems. They are six consequences of one decision:
The Person's durable identifier is derived from a key, and authority over it must nevertheless evolve.
Trace each:
| Blocker | Question | Why the choice forces it |
|---|---|---|
| O17 | How does a first-contact peer get a trustworthy starting document? | The identifier commits to a key, not to a genesis event, so nothing self-certifies the document's origin. |
| O8 | How does a verifier learn the current root after rotation? | to_verifying_key() recovers the genesis key forever, so the identifier points at the wrong authority the moment it changes. |
| O16 | Which branch is legitimate when a compromised genesis key forks the chain? | The chain is anchored at a key that can never be retired, because retiring it would break the anchor. A permanent root is a permanent forking capability. |
| O18 | How are anchor-derived Persons modelled? | Anything that is not a key cannot be an identifier — so a legitimately non-key subject (F3) had to be smuggled through new_unchecked, producing F1 and F2. |
| O13 | How is GOV_OP_V1 retired convergently? |
Did carries no principal type tag and no version position, so no receiver can classify an author or place a cutover. |
| O9 | How is a device revoked convergently? | An authorization is signed once against an authority that changes, and there is no order relating the two. |
O16 deserves emphasis because it is the deepest: a key-derived durable identifier makes its genesis key a permanent master secret. That directly violates R2.4 and R9.5, and no amount of chain machinery repairs it — the chain's anchor is the master key.
REJECTED: the key-derived durable Person identifier (PRINCIPAL_MODEL §4's Person row, and Model A of §7). Not because it is inelegant, but because it mathematically entails a permanent unretireable root, and every one of the six blockers is downstream of that.
O9 is the one blocker that is not dissolved by fixing the identifier, because it is not a consequence of the choice — it is a consequence of physics. §6.1.
6. The reframe: three moves, two of them forced
6.1 Move 1 — stop asking for global convergent revocation
Classification: RECOMMENDED, on a bounded formal argument — not ESTABLISHED. (An earlier draft called this "provably unachievable" and classified it ESTABLISHED. That was an overstatement, corrected in adversarial review; the correction is recorded here rather than quietly removed.)
The argument has two steps and one honest caveat.
- Requiring all peers to agree on whether an act preceded a revocation is requiring atomic broadcast. Chandra & Toueg, Unreliable Failure Detectors for Reliable Distributed Systems, JACM 43(2), 1996: "Consensus and Atomic Broadcast are equivalent in asynchronous systems with crash failures… a solution for one automatically yields a solution for the other."
- Deterministic consensus is unsolvable in the pure asynchronous model with crash faults. Fischer, Lynch & Paterson, JACM 32(2), 1985: "every protocol for this problem has the possibility of nontermination, even with only one faulty process."
The caveat, stated because omitting it would be misuse. FLP is routinely circumvented — by randomization (Ben-Or), by partial synchrony (DLS, PBFT), and by unreliable failure detectors. Chandra & Toueg is itself a positive result: it shows ◊W suffices to solve consensus. Citing only its equivalence half while suppressing its solution half would be quoting a paper against its own thesis.
So the correct claim is not "global convergent revocation is impossible." It is: global convergent revocation requires adopting a synchrony assumption, randomization, or an ordering authority — and ICN declines all three. #2469 excludes consensus by design; R4.1 forbids clock-based synchrony; and
icn-kernel-api::Coordinationis a Raft-shaped trait with zero implementors (§3.7). That is a design choice with a cost, and it is reopenable by paying the cost, not a theorem that forecloses it.
⇒ O9, as posed — "how is a device revoked convergently at all" across the network — has no solution within ICN's stated model. PRINCIPAL_MODEL was right that all three candidate shapes fail; it stopped one step short of the reason, which is that the target is unreachable given premises ICN has chosen, rather than merely unbuilt.
The survey confirms it empirically: no deployed system achieves decentralized global convergence on revocation. Every one either supplies an ordering authority (Signal Sesame's central server; Matrix's homeserver; WebAuthn's RP database; BitstringStatusList's issuer endpoint), only detects divergence (Certificate Transparency, CONIKS, KEYTRANS — and draft-ietf-keytrans-architecture-09 §3.3 concedes detection needs "a connected graph of all users", so a partition defeats it), or deliberately discards the ambiguous case (DIDComm: "The message recipient MUST ignore those messages"). KERI detects duplicity and resolves it only per validator — "the first seen version of an event is the authoritative one for that validator" — so partitioned validators diverge by design.
The escape hatch is formal. Guerraoui, Kuznetsov, Monti, Pavlovic & Seredinschi, The Consensus Number of a Cryptocurrency, PODC 2019: "the consensus number of an asset transfer object is 1… a more general k-shared asset transfer object where up to k processes can atomically withdraw from the same account… has consensus number k."
Consensus number 1 means the object is wait-free implementable without consensus — but be precise about what that buys:
- The primitive it needs is Byzantine reliable broadcast (BRB), not "gossip". BRB guarantees non-equivocation among receivers of a delivered message; it does not stop a Byzantine single writer from authoring two conflicting events in the first place. That residual is exactly the duplicity case §9.2 has to handle explicitly.
- ICN does not have BRB.
GossipEntrycarries no signature field at all (§3.7), so building the log's replication path is "implement Byzantine reliable broadcast", not "add a topic". §19.1 slice N3 is costed accordingly.
This gives the design its single most important constraint: keep the authority log single-writer. The moment two devices can independently author authority changes for the same subject, k > 1 and the system is back in consensus territory.
6.2 Move 2 — make the identifier commit to an event, not to a key
A self-addressing identifier — SAID = H(inception_event) — is
self-certifying without being a key. Given the identifier and the inception
event, any peer verifies the binding by hashing. This is KERI's construction, and
it is the only one in the survey that discovers current authority with no
online authority, no clock and no consensus (by replaying a log anchored at a
self-certifying inception).
Two consequences fall out immediately:
- O17's authenticity half dissolves. The genesis document is the thing the identifier commits to, so there is no trusted starting document to obtain. Its acquisition half does not: a truncated-but-valid prefix verifies perfectly and yields a stale authority state, so completeness remains open (§18, O-N5).
- O16 is narrowed by pre-rotation — for rotations only. KERI's inception commits a digest of the next key set; rotation reveals it. A compromised current key therefore cannot rotate, because it does not hold the pre-committed next keys.
Pre-rotation does not prevent forking in general, and an earlier draft of this section wrongly said it did. (Corrected in adversarial review.) It constrains establishment events only. A compromised current key can still author a competing non-establishment event — an
authorizeor arevoke— at the next position, because those are signed by a currently-authorized key and commit to nothing held in cold storage.This matters twice over. First, the fork is real. Second, and worse, a naive "every honest holder refuses to advance past the fork" rule turns a key compromise into permanent denial of identity: the victim can never publish the revocation that would remove the attacker, because the log is wedged.
The mitigation is KERI's superseding recovery, and it is adopted here: "a rotation event overrides or supersedes an interaction event with the same event sequence number", after which the witness "will no longer accept any new events of any type into the disputed branch." So the victim recovers by rotating at the disputed position using pre-rotated material the attacker does not hold. KERI's own honest concession applies unchanged: "Recovery may create an unavoidable race condition but the special rule minimizes the extent of that race condition."
Net: forking a rotation requires cold-key compromise; forking a non-establishment event requires only current-key compromise and is recoverable but racy. Residual policy is O16′ (§17), and it is a liveness question, not only a selection question.
6.3 Move 3 — let the acceptor set recency, and let the subject own the order
Rivest, Can We Eliminate Certificate Revocation Lists?, FC'98 — 28 years old and still the correct architecture here:
"Proposition 1: Recency requirements must be set by the acceptor, not the certificate issuer. The reason is that the acceptor is the one who is running the risk if his decision is wrong." Corollary 1: "Periodically-issued CRLs are wrong." Proposition 2: "The signer can (and should) supply all the evidence the acceptor needs, including recency information."
Applied to ICN: an institution is the acceptor. It decides how fresh a member's authority evidence must be. The member's device supplies that evidence with the act. Nobody needs a global agreement, because no global decision is being made — each institution decides for itself, about its own state, which is exactly the scope in which it already has legitimate authority.
This also explains why ICN's one working revocation mechanism works: RPC and gateway token revocation (AUTHORITY_SPINE §2) is scoped to a single relying party with a durable store and a fail-closed read. It is not an accident; it is the only shape that can work.
6.4 What the external survey teaches, architecture by architecture
Primary specifications, not summaries. Adopted means the idea is used in §9; rejected means it was examined and does not fit ICN's premises.
| Architecture | The one thing it teaches | Verdict |
|---|---|---|
| W3C DID Core | The four verification relationships (authentication, assertionMethod, capabilityInvocation, capabilityDelegation) are a clean device least-privilege vocabulary. But its own §9.8 says that positioning a signature in time requires anchoring "on a blockchain", and otherwise "the only safe course is to disallow any consideration of DID state with respect to time". |
data model adopted; resolution rejected |
| did:key | The correct primitive for genesis and the wrong one for a durable subject. Its spec says so: "they cannot be updated or deactivated", "any change to the cryptographic key… effectively creat[es] a new identity", and a section titled "Long Term Usage is Discouraged." This is precisely what ICN's Did is being used as today. |
diagnosis adopted |
| did:peer / DIDComm | Relationship-scoped identifiers with no registry — the direct precedent for per-context subjects. But its rotation answer is "The message recipient MUST ignore those messages": determinism bought with silent message loss, which violates R4.6. | scoping adopted; rotation rejected |
| VC 2.0 + BitstringStatusList | Portable standing is real and worth having (§17, O6). Its revocation is not: status checking mandates an online fetch (STATUS_RETRIEVAL_ERROR when unreachable), and its own privacy section concedes correlation below ~131 072 entries — unattainable per cooperative. |
credentials adopted as a layer; status lists rejected |
| KERI | The closest architecture to ICN's premises, and the source of the two mechanisms §9 uses: self-addressing identifiers and pre-rotation. Also the honest limit: duplicity is "first seen… for that validator", so partitioned validators diverge by design. | mechanisms adopted; the stack (witnesses, watchers, CESR, OOBI) rejected as too heavy for ICN's stage |
| Key transparency / CT / CONIKS | Non-equivocation is detection, never a decision. draft-ietf-keytrans-architecture-09 §3.3: detecting a fork needs "a connected graph of all users", else the log "can attempt to partition users into subsets that do not gossip". | rejected — it yields an alarm, not a verdict |
| WebAuthn / passkeys | The best deployed device-holder proof and UX, and a model of correlation resistance: "Relying Parties are not able to detect any properties, or even the existence, of credentials scoped to other Relying Parties." Revocation is purely the RP's database. | adopted as a device-key gate; rejected as a subject identifier |
| UCAN / ZCAP-LD | Attenuating delegation chains are the right shape for R3.2, and UCAN's revocation set is deliberately a grow-only, order-insensitive CRDT. But it concedes it does not undo an already-honoured invocation — convergence on the set, not on the effect. | attenuation adopted; revocation-as-answer rejected |
| FROST (RFC 9591) | A stable group public key with membership changing beneath it — the shape a threshold-held institution key would need. But "FROST does not provide robustness", DKG is out of scope, and signing needs t reachable participants in one session. |
deferred — see O12 |
| BBS+ / anonymous credentials | The only design offering cross-institution unlinkability and third-party verifiability. Requires a BLS12-381 pairing stack, is still Internet-Draft, and its revocation story falls back to status lists. | deferred — and F16 means ICN could not wire it today regardless |
| Signal Sesame / Matrix cross-signing | Two shipping patterns worth stealing: tombstone-with-bounded-window (Sesame keeps stale device records until MAXLATENCY so delayed messages still decrypt) and receive-time trust re-evaluation (Matrix: "messages sent from non-cross-signed devices… SHOULD NOT be displayed"). Both assume a central ordering authority ICN does not have. |
patterns adopted (§9.3); central authority rejected |
The cross-cutting lesson. Of every architecture surveyed, exactly one discovers current authority with no online authority, no clock and no consensus — KERI, by replaying a single-writer log anchored at a self-certifying inception. That is not a coincidence: it is the only one built from the same premises ICN's R1/R4/R7 state.
6.5 What the three moves buy
This table is a summary of §18 and must never diverge from it. (An earlier draft marked O13 and O17 "dissolved" here while §18 correctly called both partial; review caught the mismatch, and readers of the summary alone could have treated unresolved prerequisites as closed.)
| Blocker | Disposition | Remaining condition |
|---|---|---|
| O8 | DISSOLVED by 6.2 — current authority is derived by replaying the subject's own log from a self-certifying inception | — |
| O18 | DISSOLVED by 6.2 + §10 — a non-key durable subject becomes expressible, so nothing needs new_unchecked |
the anchor↔member keying trace in §15.2 is still incomplete |
| O17 | PARTIAL — self-addressing settles authenticity of the inception event | acquisition/distribution is not settled: a truncated-but-valid prefix verifies and yields stale authority. §18, O-N5 |
| O16 | NARROWED, not defused — pre-rotation covers establishment events only | non-establishment forks remain; recovery is by superseding rotation and is racy. O16′ |
| O13 | PARTIAL — per-subject declaration beats a global cutover | O13 constrains receivers; a subject who never declares keeps v1 alive, which R12.3 [HARD] forbids. Receiver-side retirement unsolved |
| O9 | REJECTED as posed by 6.1 — unachievable within premises ICN has chosen (not a theorem-level impossibility; see §6.1) | replaced by the per-act-class requirement in §9.3, which itself carries a known R5.1 gap until O-N6 |
7. Candidate models
Five models, each stated at its strongest. None is a straw man; A is the repo's own current proposal and D is the position most of the external ecosystem holds.
Model A — Key-as-Person (the status quo, plus the A0 program)
The Person identifier is a self-certifying key-derived Did. Authority evolves
through an authenticated identity-document chain anchored at the genesis key the
DID encodes. Device authority travels as a carried, self-contained
DeviceAuthorization.
Strongest case. Nothing new to build for genesis — it already works offline
(§3.2). Verification needs no resolution, no registry, no network round-trip:
author.to_verifying_key() and done, which is exactly why #2469 could freeze a
field set. One identifier everywhere means membership records, storage keys and
the whole existing type surface keep working. The chain's cheap parts already
exist: capability checks, Ed25519 verification, strict +1 monotonicity.
Why it fails. §5: the identifier's binding to the genesis key makes that key a
permanent, unretireable master secret, which is a direct R2.4/R9.5 violation
and the root of O16. O8, O17, O13 and O18 follow. It also fails R6.1 outright —
one permanent 32-byte value in every membership record, every gov:vote:{pid}:{voter}
key and every gossiped op. REJECTED.
Model B — Stable non-key Person anchor, globally resolvable
A durable Person identifier exists independent of keys (random bytes, or a hash);
keys and devices are authorized for it. This is what SDIS's Anchor actually is
(F3).
Strongest case. It gets R2.1/R2.2 right — subject and authority are genuinely
separate, rotation is natural, and there is no permanent genesis key. It matches
the strongest existing ICN design intent, and KeyBundle::did() already carries
the doc-comment "Returns the anchor's DID, not a key-based DID".
Why it fails. Three ways. (i) Genesis has no authority root: nothing stops a second party claiming an unclaimed anchor, and first-writer-wins in a global namespace is arrival-order dependent — the exact rule #2469 §8 withdrew as non-convergent. (ii) Resolution requires a global authenticated directory, so you inherit key-transparency's split-view problem, whose own architecture draft concedes detection needs "a connected graph of all users". (iii) Correlation is worse than Model A, because a permanent non-rotating anchor cannot even be changed. REJECTED as a global identifier; its core idea — a durable non-key subject — is retained in §9 and made self-certifying and per-context.
Model C — Context-scoped identity with a private continuity root
There is no global Person identifier. The person holds keys and a private continuity root. Each institution knows them by a distinct, self-certifying subject identifier whose authority evolves in a log the subject alone writes. The institution is the relying party.
Strongest case. Satisfies R6.1 structurally rather than by policy. Removes the global namespace and therefore O8/O16/O17 in their global form. Single-writer authority logs have consensus number 1 (§6.1), so no consensus is required. Matches the only external designs built on the same premises (did:peer's relationship-scoped registry; KERI's per-relationship AIDs).
Costs, stated up front. Recovery is per-context work, not one act (§12). Cross-context proof requires an explicit linkage step (§11.3). Sybil resistance needs a nullifier, and ICN's ZK stack cannot currently provide one (F16). "Who is this person, globally?" becomes unanswerable — which is usually correct and occasionally inconvenient.
Two costs an earlier draft omitted, both found in adversarial review, and both worse than the ones above.
- The continuity root has a larger blast radius than Model A's genesis key. It is the person's index of their own subjects plus the material that recovers each one, so compromising it is simultaneously takeover of every context and full cross-context deanonymization — it hands the attacker exactly the linkage map §11 exists to prevent anyone else from building. Model A's rejected genesis key compromises one identity. This model concentrates what Model A distributes, and §12.2 path (b) exists because of it.
- Availability is a per-subject dependency, not zero infrastructure. A verifier needs the subject's log prefix; when it is missing, someone reachable must serve it. Model B was rejected partly for needing one global directory; Model C needs N per-subject availability paths. They are materially different — a relying party can serve the logs of its own members, so the dependency is per-relationship rather than global and universal — but "no centralization dependence" is too clean, and the matrix below is corrected.
Model D — Credential-centric, no person identifier at all
The person holds keys; institutions issue verifiable credentials; every relationship is a credential. Standing is proven by presentation.
Strongest case. Maximum portability — standing provable to a third party that
holds no registry, which is precisely the gap PRINCIPAL_MODEL §7.2 names. Aligns
with the largest external ecosystem (VC-DM 2.0). Institutions already behave this
way: FounderSignature (F18) is a credential in all but name.
Why it fails as the whole answer. (i) A credential subject still needs an
identifier, so pairwise identifiers reappear — D collapses into C plus a
credential layer. (ii) Credential revocation is the same impossibility again,
and the ecosystem's answer is BitstringStatusList, which mandates an online fetch
(STATUS_RETRIEVAL_ERROR if unreachable) and whose own privacy section concedes
correlation below ~131 072 entries — unattainable per cooperative. (iii) It
supplies no continuity mechanism: lose every device and there is nothing that
is still you. REJECTED as the whole answer; adopted as a layer (§11.2, §14).
Model E — Hybrid identity/authority graph
Continuity anchor + authority keys + pairwise identities + recovery authorities + institutional credentials, all first-class.
Strongest case. It can express every requirement, and every component has a real justification somewhere in §2.
Why it fails. Complexity is itself a cost, and here it is disqualifying: five
first-class concepts means five genesis stories, five recovery stories and five
revocation stories, whose interactions are where security bugs live. §3 is a
catalogue of what happens when ICN maintains several parallel identity models at
once — three "steward vouch" authority models that disagree (F6/§3.5), two
incompatible DidDocument types, two contradictory definitions of "who may sign".
E institutionalizes that failure mode. REJECTED — but note that C is E with
four of the five concepts demoted from protocol objects to client-side or
app-layer concerns, which is the whole point.
Evaluation matrix
Qualitative, with reasons rather than false precision. ✔ meets · ~ partial / costed · ✘ fails · n/a not applicable.
| Criterion | A key-as-person | B global anchor | C context-scoped | D credential-only | E hybrid |
|---|---|---|---|---|---|
| Self-sovereignty (R1.1–R1.3) | ✔ | ~ needs directory | ✔ | ✔ | ✔ |
| No centralization dependence | ✔ | ✘ global directory | ~ per-subject log availability | ~ status lists | ~ |
| Offline genesis (R7.1) | ✔ | ~ claim race | ✔ | ✔ | ✔ |
| Device compromise bounded (R3.2) | ~ | ~ | ~ bounded only while ≥1 key survives (§9.3) | ~ | ~ |
| Root compromise survivable (R2.4) | ✘ permanent master | ✔ | ✘ path (a) — and unrecoverable; ✔ path (b) | n/a | ~ |
| Total-loss recovery (R9.1 [HARD]) | ✘ O14 — chain cannot advance | ~ | ~ admitted shortfall — (a) fails if the backup is lost; (b) needs threshold signing ICN lacks (F7) and which is absent from §19.1 | ✘ nothing persists | ✔ |
| Multi-device (R3) | ~ roster only | ~ | ✔ | ~ | ✔ |
| Least privilege (R3.2) | ✘ #2590 | ~ | ✔ attenuated | ✔ | ✔ |
| Deterministic verdict (R4.3) | ~ | ✘ split view | ✔ single-writer | ✘ fetch may fail | ~ |
| Partition tolerance (R7.3) | ~ | ✘ | ✔ | ✘ | ~ |
| Historical signature validity (R2.3) | ~ ambiguous after recovery | ~ | ✔ anchored position | ✔ | ~ |
| Correlation resistance (R6.1) | ✘ | ✘ worse | ✔ | ✔ | ✔ |
| Selective disclosure (R6.2) | ✘ | ✘ | ~ needs new crypto (F16) | ✔ | ✔ |
| Governance authorship (R8) | ~ blocked O8/O9 | ~ | ✔ | ~ | ✔ |
| Institutional delegation | ~ | ~ | ✔ | ✔ | ✔ |
| Hosted-node independence (R11) | ✔ | ✔ | ✔ | ✔ | ✔ |
| User experience (R10) | ✔ one identity | ✔ | ~ many contexts, hidden by the app | ~ | ✘ |
| Implementation complexity | ~ | ✘ directory | ~ | ~ | ✘ |
| Protocol complexity | ~ | ✘ | ~ | ~ | ✘ |
| Migration complexity | ✔ none | ✘ | ~ low, not zero — keys and custody are untouched, but every existing DID needs a new SubjectId plus a membership bridge (§15.1). (Was scored "near-zero" before review round 3 showed a DID cannot be a subject identifier.) |
✘ | ✘ |
| Cryptographic agility (R12.1) | ✘ DID pins the suite | ✔ | ✔ | ✔ | ✔ |
| Auditability (R9.2) | ~ | ~ | ✔ append-only log | ~ | ~ |
| Meaning Firewall (R13.1) | ✘ Person in kernel |
✘ | ✔ §9.5 | ✔ | ✘ |
Selected: Model C — on the balance of the table, not on a clean sweep. Three cells above are shortfalls the model does not discharge: R2.4 under the default recovery path, R9.1 [HARD] under both recovery paths, and per-subject log availability. §9.6 carries them forward; a reader who weighs R9.1 above correlation resistance should reach a different answer, and that is a legitimate reading of the same evidence.
8. Adversarial scenarios
All 25 mission scenarios, walked against Model C as specified in §9, with the discriminating failures of the rival models noted. A scenario that Model C cannot answer is marked OPEN and appears in §17 — none are silently absorbed.
| # | Scenario | Model C outcome | Where A/B/D/E differ |
|---|---|---|---|
| 1 | Genesis key compromised 5 yrs after a legitimate rotation | Contained. The identifier commits to the inception event, not to a key, and rotation revealed pre-committed next keys. A stolen genesis key can sign nothing current and cannot rotate — it does not hold the pre-rotated material. | A fails catastrophically — the genesis key is the chain anchor forever (O16). |
| 2 | Attacker and user each create a same-generation successor | Split. Forking a rotation requires cold-key compromise. Forking a non-establishment event (authorize/revoke) requires only current-key compromise (§6.2). Duplicity is detected; the victim recovers by superseding rotation at the disputed position. Racy, and OPEN as O16′ — which is a liveness question, not only a selection one |
A: both branches verify from genesis; a content tiebreak converges on the attacker. |
| 3 | Two concurrent recovery attempts | Recovery is per-context (§12). Each institution runs its own delay-and-veto and reaches one outcome. Contexts can diverge — the person may recover Coop A while an attacker takes Coop B. That is a real cost, and it is bounded blast radius rather than a global race. Stated, not hidden. | A/B: one global race decides everything at once. |
| 4 | One trustee submits the same proof M times | Rejected — thresholds count distinct authenticated participants (R9.4). Today this fails: sync.rs:263-269 has neither signature check nor dedup (#2591), and SDIS's approve_by_steward() is a bare += 1. |
same for all models; a requirement, not a differentiator |
| 5 | Threshold trustees collude | Not fully mitigated, by design. Mitigations: per-context recovery means colluding guardians must pass each institution's procedure; delay + subject veto (R9.6); recovery is a logged event, never silent (R9.2). Residual risk stated. | E claims more; the extra machinery does not remove the trust |
| 6 | Phone stolen while disconnected two weeks | The thief acts within the device's attenuated scope. For class 1 the window binds once decisions pin an evaluation position (O-N6); for class 2 it does not bind at all — the thief keeps referencing the original position, so the authorization stays acceptable indefinitely. R5.1 is not met for class 2 (§9.3); the real controls are scope limits and the acceptor's own freshness bar | A: unbounded, and with no recovery path at all |
| 7 | Revoked phone signs a governance action and delivers it late | Per act class (§9.3). Deferred-decision acts (votes): the decision pins a log position, so the act is evaluated against converged state and does not count. Settled acts: revocation is prospective only; the act stands and the bound is scope, not retroaction. | No model solves this globally — §6.1 proves it impossible. A/B claim to and cannot |
| 8 | User loses every device | Recovery from the continuity root, per context (§12) — if the backup survived. Lost devices and backup is answered only by guardians, which need threshold signing ICN lacks (F7). R9.1 [HARD] is not fully met | A: impossible today — RotationEvent::verify needs an existing non-revoked key (O14). D: nothing persists |
| 9 | Replacing a ten-year-old phone | Ordinary device authorization + revocation in the subject's log. Invariant test T1 passes. | all pass |
| 10 | Signature algorithm deprecated | Rotate to a new suite; the identifier is a hash of an event, not of a key, so it is unchanged (R12.1). | A fails — the DID is an Ed25519 key |
| 11 | Gateway compromised | It relays; it holds no subject key and cannot author (R8.1/R8.4). It can withhold or delay — unmitigated and stated. | all models that separate relay from authorship |
| 12 | Personal node compromised | Same as 11. The node is a distinct principal; T4 passes. | — |
| 13 | Malicious hosting provider | Cannot become the institution: institutional acts are person-signed mandate chains, not host-key signatures (§14). T3 passes. Can deny service — stated. | — |
| 14 | Institution impersonates a member | Cannot: the institution holds no key that derives the member's subject, and acts carry a device signature the institution cannot produce. | today this fails — #2589 mints a token for an unproven subject |
| 15 | Member wants Coop A and Coop B unable to correlate them | Satisfied by construction — distinct subject identifiers, distinct per-context device keys (§11.1). | A and B fail — one identifier everywhere |
| 16 | Member wants to prove two personas are one human | Sign a linkage statement with both subjects' current keys (§11.3). Cheap and consent-gated. | A: trivial but always-on, i.e. not consent-gated |
| 17 | Federation needs a delegated mandate without learning unnecessary identity | The delegating coop attests "this subject is our delegate"; the federation learns the federation-scoped subject only. Note the coop does learn both — necessarily, since it created the delegation. Correlation follows the relationship, not the identifier. | — |
| 18 | User has no personal node | Unaffected — identity is client-side; a gateway relays. R11.1. | all pass |
| 19 | User runs three personal nodes | Unaffected — nodes are separate principals. T4/T5 pass. | all pass |
| 20 | User signs while partitioned | Works: signing is offline (R7.2); the act references a log position the device already holds. | — |
| 21 | Device authorization arrives before/after a root rotation, in different orders | Both are events in one single-writer log with +1 monotonicity, so their relative order is fixed by the log, not by arrival. Out-of-order delivery buffers, never rejects (R4.6). |
A cannot place them at all — this is exactly O8's collapse into O9 |
| 22 | Revocation and a member act arrive in opposite orders | Admission is monotone and order-independent; effect is computed from converged state at a pinned position (§9.3). Two honest nodes agree. | A/B: arrival-order divergence — PRINCIPAL_MODEL §6.5 refutes both shapes |
| 23 | An old receipt must stay verifiable after recovery | Only before the earliest fork. For positions preceding any fork, "K was authorized at N" is permanent (append-only, hash-chained) and R2.3 holds. After a superseding rotation at p, receipts from orphaned positions > p keep a verifying signature but lose their authorization proof — they degrade to "authored on a superseded branch" (§9.2.1, O-N9). (An earlier draft claimed an unqualified yes.) |
A: recovery replaces the root and history becomes ambiguous everywhere |
| 24 | Migrating an SDIS anchor identity | Near-free. F5 (the anchor was never the acting principal) and F20 (mutating routes disabled) mean no deployed anchor carries authority; F4 shows the VUI feeding it is SHA256(did), so none carries uniqueness either. §15.2. |
A: O18 is listed as critical precisely because it assumed otherwise |
| 25 | An existing key-derived DID holder must not "become a new person" | Their key, custody and signing ability are untouched — no ceremony, no re-enrollment. A new SubjectId is allocated whose inception names their existing Did as initial authorized key; the identifier changes, the person does not, and membership rows keyed by Did need a documented bridge (§15.1). |
B/D/E require a full cutover |
Scenarios Model C does not fully answer, carried to §17: 2 (residual duplicity policy), 3 (cross-context divergence under concurrent recovery), 5 (guardian collusion), 7 (irreducible — §6.1), 11 (relay withholding).
9. The recommended architecture
Classification: RECOMMENDED. It is the strongest model after comparison, not a forced conclusion. §17 lists what would reopen it.
9.1 Four objects, each with exactly one job
| Object | Definition | Layer | Public? |
|---|---|---|---|
| Principal | a keypair. Its public key is a Did. |
kernel | yes, to whoever verifies a signature |
| Subject | SubjectId = H(inception_event) — a self-addressing identifier, not a key. One per context. |
identity | to that context only |
| Authority log | a per-subject, single-writer, hash-chained, +1-monotone log of authority events, with pre-rotation |
identity | replicated to that subject's relying parties |
| Continuity root | a private client-side secret: the person's index of their own subjects, plus the pre-rotation material that lets them recover each one | client only — never published, never sent | no |
The person is not an object in the protocol. That is the point. Person is an
app-layer word for the human who holds a continuity root, and no kernel type
needs to know it exists (R13.1).
Why the subject is a hash of an event, not a key. §4 proved a durable subject
must not be a key; §6.2 supplies the alternative. Given SubjectId and the
inception event, any peer verifies the binding by hashing — self-certifying with
no registrar, no resolver and no trusted starting document. That answers O17's
authenticity half only — completeness of the prefix you were handed is a separate,
still-open question (§18).
9.2 How authority evolves
The inception event declares the subject's initial authorized keys and commits to a digest of the next key set (pre-rotation). Each later event is signed by a key authorized in the immediately preceding state and advances the position by exactly one.
inception { keys: [K0], next: H(K1set), policy } position 0
│ SubjectId = H(this event) ← self-certifying, no registry
▼
authorize { device: D1, scopes, valid_for } position 1
▼
rotate { reveal K1set, next: H(K2set) } position 2
▼
revoke { device: D1 } position 3
Four properties, and each does specific work:
- Single-writer ⇒ consensus number 1 (§6.1), given Byzantine reliable broadcast underneath — which ICN does not have yet (§6.1, §19.1 N3).
- Pre-rotation means a compromised current key cannot rotate. It does not prevent that key forking a non-establishment event — see §6.2. Scenario 1 contained; scenario 2 narrowed, not eliminated.
+1monotonicity makes out-of-order delivery buffer, never reject (R4.6) — precisely the gap-fill obligation PRINCIPAL_MODEL's A0 identified, now with a reason it is safe.- Duplicity is detected, and recovered from by superseding rotation (§6.2).
Do not let two devices write this log. The instant two devices can independently author authority events for one subject, k > 1 and consensus is required again (§6.1). Concurrency here is a correctness boundary, not a feature request.
9.2.1 The replicated state model
Three successive drafts got this wrong, and all three errors are recorded because
each is instructive. (a) The first called the merge "a monotone join, i.e. a
CRDT" while also saying holders "refuse to advance past the fork" — which cannot
both hold. (b) The second proposed (longest verified prefix, set of duplicity positions); review supplied the counterexample — if A advances through X@n and B
through Y@n, recording the duplicity needs either branch selection (no authority)
or prefix truncation (non-monotone). (c) The third stored whole events in a set;
review pointed out that Ed25519 admits more than one valid signature per message, so
that is really a keyed map with an undefined value merge — and any rule for picking
a signature is a content-based tiebreak that breaks commutativity. The lesson each
time: a verified prefix is a conclusion, and a signature is not part of identity.
Digests, stated once and used everywhere
event_id = H(canonical_body) ← signatures are OUTSIDE every digest
SubjectId σ = event_id(inception body)
prev_digest = event_id(parent body)
All three must use the body digest. If σ or prev_digest hashed the whole
event, a hedged or retrying signer re-signing an identical body would mint a second
SubjectId — or a child would name a parent variant that dedup discards, wedging
the chain permanently. That is precisely the false duplicity the body rule exists to
remove, relocated one layer down.
Durable state — two grow-only sets, whose elements are bodies
For subject σ:
Bodies(σ) = { b : ∃ s. admissible(b, s) ∧ b.subject = σ } -- canonical bodies
Witness(σ) = { (event_id(b), s) : admissible(b, s) } -- signatures, never compared
admissible(b, s) is state-independent — decidable from the bytes alone:
- canonical encoding parses; version and domain separator are as specified;
sverifies underb.signer_key, carried inline (this proves only "some key signed this body" — authority is a derived question);- for an inception body,
σ == event_id(b); otherwiseb.subject == σ; b.position ≤ MAX_POSITION, an absolute protocol constant — notlocal_frontier + C, which would make admissibility depend on local state and break the join.
- Partial order: componentwise
⊆. Join: componentwise∪. - Associative, commutative, idempotent: inherited from set union. ∎
- Receipt order cannot change durable state — union discards order.
- Monotone: nothing is removed by the join.
Why bodies and not events. Ed25519 permits several valid signatures over one
message. With whole events as elements, (id, s₁) and (id, s₂) are two elements at
one position — manufacturing duplicity that halts the subject — and collapsing them
requires choosing a signature, which is a content tiebreak and destroys
commutativity and idempotence. With bodies as elements, re-signings collapse
automatically and Witness simply accumulates; nothing ever compares two signatures.
Representation of the awkward cases falls out for free:
| Case | Representation |
|---|---|
| gap | Bodies(σ) contains no body at that position. No sentinel |
| duplicity | two distinct bodies at one position. No flag |
invalid (fails admissible) |
never enters — and because the test is state-independent, every replica agrees |
| re-signed identical body | one element, several witnesses. Not duplicity |
| unauthorized but well-formed | is in Bodies(σ), and is never selected by the derived view. Spam cannot manufacture duplicity |
State-independence is what makes the filter commute with union —
filter(A) ∪ filter(B) = filter(A ∪ B) — and it is why the bound in rule 4 must be
an absolute constant.
Admission is cheap, so the durable set is attacker-writable, and "retention affects liveness not safety" was too strong. Anyone can mint unlimited admissible bodies for any public σ by signing arbitrary bytes with a fresh key. They can never enter
C(they are unauthorized), so they cannot fork anything — but they are a storage and bandwidth attack, which forces eviction, and eviction is a per-replica function. Under adversarial shaping, "convergence over the eventual retained set" is close to vacuous.Two consequences, both stated rather than absorbed: the eviction rule must be protocol-specified rather than implementation-chosen, so that two honest replicas under the same attack retain the same set; and per-signer admission cost belongs in the replication layer (N3), where it may be state-dependent without touching the join.
Eviction must be authority-aware, or spam evicts legitimate authority events. (Raised in review round 3 and correct.) A position bound alone does not help: an attacker floods
frontier + 1with self-signed bodies that correctly name the verified prefix, and sinceadmissibleis state-independent it cannot tell them from the legitimate next event. A naive bounded store then discards the real one, and anti-entropy cannot restore it while the cap stays full — so receipt order would decide which replicas ever derive it.The distinguishing predicate exists, just not in
admissible:authorizedin the derived view separates them perfectly, because spam is signed by keys the log never authorized. Eviction may therefore consult derived state — it is a lossy local operation, not part of the join — and the rule is retain authorized bodies before unauthorized ones, which makes spam at or below the frontier harmless.The residual is the gap region and it is not solved here. Beyond the frontier, with intervening positions missing,
authorizedcannot yet be evaluated, so far-future spam is indistinguishable from legitimate future events. Retention should prefer proximity to the frontier and a carried chain-inclusion witness makes the far-future case decidable — but until that rule is specified, R4.6 and permutation-invariant convergence hold only absent adversarial storage pressure, and this document does not claim otherwise. Recorded as O-N8 (which now covers admission and compaction), with O-N5 for the topology.
Derived view — a pure function of Bodies(σ)
derive(Bodies(σ)):
b₀ ← the unique b with σ = event_id(b) # unique by construction
if none: return Unknown
state ← apply(b₀); p ← 1
loop:
C ← { b ∈ Bodies(σ) : b.position = p
∧ b.prev_digest = event_id(chosen(p−1))
∧ authorized(b, state) }
C ← supersede(C)
match |C|:
0 → return Live(state, frontier = p)
1 → state ← apply(state, c); p ← p+1
_ → return Halted(state, disputed_at = p, candidates = C)
Inception cannot fork: two different inception bodies hash to two different σ,
so they are two different subjects.
The recursion is well-founded: derive advances one position at a time and only
after |C| = 1, so chosen(p−1) is defined whenever C at p is computed; if
p−1 halts, derive has already returned.
The derived view may regress; the durable state may not. That is a conclusion
changing on new information, and it is convergent — every replica holding the same
Bodies(σ) computes the same verdict.
derive is pure only if authority principals are keys. Every principal named
inside an event — device, guardian, recovery authority — must be a raw Did,
verifiable from bytes. If a guardian were named by SubjectId, authorized would
depend on another subject's log, which may itself be gapped or halted, and mutual
guardianship would make the recursion non-well-founded. Normative for N1.
supersede — the one selection rule, and the constraints that make it safe
supersede(C) = if C contains any establishment event
then { b ∈ C : b is an establishment event }
else C
Establishment events (rotations, recovery) supersede non-establishment events (authorize, revoke) at the same position — KERI's superseding recovery, and the authenticated selector O16 needed. It selects by authority class, never by content: no lowest-hash, no first-seen, no longest-chain. Where it cannot select on authority it halts.
Three constraints are load-bearing. Violating any one turns the selector into an attack:
Establishment eligibility is defined by a pre-rotation pre-image reveal — never by a capability flag, and no delegation may mint it. If
recoverwere establishment because the signer holds aRecovercapability, an attacker with a compromised current device would author an establishment event and winsupersedeagainst the legitimate holder's rotation — authority-based selection becoming attacker-based selection. F13/F13a in §19.3 is exactly such a route in today's code. This also explicitly bars §9.5's generic attenuating delegation from delegating establishment authority. Same class of error as #2590, one layer up.Rotation bodies must be deterministic. Pre-rotation commits to
H(K_next_set), not to the rotation body — so the holder ofK_nextcan authorrotate{reveal K_next, next: H(K_a)}androtate{reveal K_next, next: H(K_b)}, both authorized, different bodies ⇒|C| = 2⇒ permanent halt with no attacker present (a device that crashes before persisting its new commitment and retries with fresh randomness kills the subject). The commitment scheme must therefore make the whole rotation body re-derivable — e.g.K_{p+1} = KDF(root ‖ p+1)with every other field determined — so an honest retry collapses to oneevent_id. Normative for §19.4 item 2.Exactly one authorized establishment body may exist per position — and threshold signing alone does not deliver that. (Sharpened in review round 5.) With
M-of-Nguardians signing independently, two quorums yield two authorized bodies atp⇒ permanent halt, so anyMguardians hold a kill switch, not merely a takeover. But FROST does not fix it either: a group key authenticates a quorum, it does not restrict that quorum to signing one message, and the sameMguardians can still sign two distinct bodies — as can a phrase-holder racing a guardian quorum with a different body.Two rules together are required, and both are normative for N1:
authorizedmust enforce canonical derivation, not merely permit it. An establishment body is authorized only if it equals the body canonically derived from the revealed pre-committed material. A non-canonical body — from a malicious quorum or an honest retry — failsauthorizedand never entersC. This is constraint 2 promoted from "re-derivable" to "checked".- Each position commits to exactly ONE next authority. Pre-rotation must not arm two alternative recovery paths at the same position; the person selects path (a) or path (b) at each rotation, never both. Otherwise the two paths reveal different material and derive two different canonical bodies, and the race returns.
With both, at most one authorized establishment body can exist at a position, so
supersedenever has to choose between two. N7 remains a hard prerequisite of guardian recovery (§12.2, §19.1) — it is necessary and, on its own, not sufficient.
What this does and does not give
| Question | Answer |
|---|---|
| Can later events become usable after a fork? | Yes, and only via a superseding establishment event at the disputed position |
| Does any rule silently select a branch? | No. supersede is explicit and authority-based; every other |C| ≥ 2 halts |
| Does BRB prevent equivocation? | No, and nothing here assumes it. BRB gives non-equivocating delivery among receivers; a Byzantine writer can still author two conflicting signed bodies — which is why derive must handle |C| ≥ 2 |
| Can one compromised current key halt a subject unilaterally? | Yes. It authors authorize{D₁}@p and authorize{D₂}@p, both authorized ⇒ Halted(p) with no victim action. Current-key compromise is an instant DoS whose only cure is a cold rotation, and if device compromise persists each revealed generation is captured in turn, burning one pre-rotation generation per round. O16′ |
| What does superseding recovery cost? | It orphans the suffix, and that also costs historical authorization evidence. Rotating at a disputed p changes chosen(p), so every honest later body whose prev_digest names the old chosen(p) can never enter C — events at p+1 … frontier are lost and must be re-authored. A fork deep in the log is far more expensive than one at the frontier. See the R2.3 note below |
| Are class-2 settled acts convergent under a fork? | No — and this is a settlement-safety problem, not a liveness one. See below |
Class-2 settlement is not convergent under a fork, and the earlier framing understated it. The regression is not only
Live → Halted; it isLive(branch A) → Live(branch B). Replica R1 receives only the attacker'sauthorize@5, derivesLive(frontier 6), and finalizes a class-2 ledger act citing position 5. R2 receives the victim'srotate@5first and rejects the same act. After merge both hold an identical set and derive an identical verdict — and hold permanently different ledger state, because R5.3 says settled effects are never unwound. That is the arrival-order divergence #2469 §8 withdrew, reintroduced through class 2.An earlier draft hid this by saying "acts at positions
< premain evaluable", as ifpwere known at admission. It is not —pis discovered afterwards.There is no fix inside this model. Reducing exposure means admitting class-2 acts only against positions deep enough to be beyond challenge, which is a finality depth — a concept ICN does not have, and one whose local evaluation reintroduces the same divergence in weaker form. Recorded as O-N7, and O16′ is reclassified from a liveness question to a settlement-safety question. Until it is answered, this authority model is not by itself a sufficient basis for irreversible settlement, and §9.3 class 2 must be read with that limit attached |
9.2.2 The model against adversarial delivery orders
Every case reasoned through against §9.2.1. The requirement in each: the same durable joined state and the same derived verdict for all honest replicas given the same eventual event set.
| # | Case | Durable state | Derived verdict | Converges? |
|---|---|---|---|---|
| 1 | n arrives before n−1 |
{eₙ} — union, no ordering |
advance halts at the gap; eₙ sits in S unused. On eₙ₋₁ arriving, advances through n |
✔ |
| 2 | n−1 before n |
same set as case 1 | same as case 1 | ✔ — 1 and 2 reach identical state |
| 3 | valid X@n, then conflicting valid Y@n |
{…, X, Y} |
Halted(n, {X,Y}) |
✔ |
| 4 | Y@n then X@n |
identical set to case 3 | identical — derive is a function of the set |
✔ |
| 5 | replicas hold X@n and Y@n separately, merge later |
S_A ∪ S_B |
both reach Halted(n) after merge; before merge each is legitimately behind |
✔ |
| 6 | identical event repeatedly | union is idempotent; keyed on event_id = H(body), so re-signings collapse to one element |
unchanged | ✔ (only because of the body-digest rule above) |
| 7 | invalid event at n before the valid one |
fails admissible ⇒ never enters S. If merely unauthorized, it enters S but never enters C |
the valid event is unaffected — spam cannot block or fork | ✔ |
| 8 | rotate@n without the pre-committed material |
enters S (it is well-formed and self-signed) |
fails authorized, so never enters C; ignored |
✔ |
| 9 | compromised current key forks a non-establishment event at n |
both bodies in Bodies(σ) |
A single compromised key needs no victim to fork. It authors authorize{D₁}@n and authorize{D₂}@n — both authorized, distinct bodies ⇒ Halted(n) unilaterally. (If it authors only one, derive advances on it: a stolen-key action, not a fork.) |
✔ converges — on a halted subject. Current-key compromise is an instant DoS; cure is a cold rotation |
| 10 | legitimate rotation recovers from case 9 | attacker's authorize@n and victim's rotate@n both present |
supersede filters to the establishment event ⇒ |C| = 1 ⇒ advance, rotating the attacker out |
✔ defined and deterministic — but it orphans the suffix. Changing chosen(n) strands every honest body at > n whose prev_digest names the old chosen(n); those must be re-authored (§9.2.1) |
| 11 | fork behind the frontier: bodies at 0–5 and 8–9, fork at 3 | all present | Halted(3). Bodies at 4, 5, 8, 9 are unreachable — derive never evaluates past 3. Rotating at 3 recovers the subject and permanently orphans 4, 5, 8, 9 |
✔ converges; the cost grows with fork depth. Case 10 alone would not have caught this |
Suffix orphaning breaks R2.3 for the orphaned range, and §8 scenario 23 was too strong. (Refuted in review round 4.) If key
Kwas first authorized at positionN > pand a superseding rotation lands atp, the derived view no longer establishes thatKwas ever authorized — so a receipt signed byKkeeps a verifying signature but loses its authorization proof.What survives, and it is not nothing:
Bodies(σ)is grow-only, so the orphaned bodies are still there. A verifier can still show "this body atNwas signed byKand chained to the pre-fork prefix" — i.e. provably authored on a branch that was later superseded. That is strictly weaker than "authorized", and the difference is exactly what a receipt needs.R2.3 must therefore be qualified: it holds unconditionally for positions before the earliest fork, and for later positions it degrades from "authorized" to "authored on a superseded branch." Preserving full accepted-at evidence across recovery would need durable per-branch endorsement records, which this model does not define. Recorded as O-N9.
Two operational consequences that are easy to get wrong.
- Recovery must rotate at the disputed position, not at the next one. If the
victim's own legitimate event also sits at
p, their view may have shownfrontier = p+1before the fork was known, and the natural instinct is to publish atp+1— which is unreachable, becausederivehalts atp. The rotation must be authored atp, wheresupersedefilters out both non-establishment events — the attacker's and the victim's own. The victim therefore loses their own event atpand must re-issue it atp+1. That is a real cost of recovery, not a bug. - This assumes the subject notices. A halted subject stays halted until its controller observes the fork and rotates. That is KERI's "sufficiently responsive controller" assumption inherited wholesale, and it means fork monitoring is a client obligation, not an optional feature. A person whose app never checks is a person whose subject can be frozen indefinitely.
Does superseding recovery terminate? Yes. The rotation at p replaces the key
set, so the attacker's compromised key is not authorized at p+1 and cannot fork
again — provided the rotation actually revokes it, which is why a recovery rotation
must replace rather than extend the authorized set.
Where it does not converge on a good outcome, and this is stated rather than
engineered around: if the attacker holds the pre-rotation material as well,
they can author an authorized establishment event at n. If the victim also does,
\|C\| = 2 after supersede and the subject halts permanently — all replicas
agree, so it is convergent, but the subject is dead rather than recovered. If only
the attacker does, they take over. Both are the backup-compromise case §12.2
records as unrecoverable, and the residual policy is O16′.
9.3 Revocation, stated per act class — the replacement for O9
Because §6.1 forbids a single global answer, the semantics is stated per class. R5.2 requires this table to be explicit; silence would be the defect.
| Class | Examples | Admission | Effect | Revocation reaches back? |
|---|---|---|---|---|
| 1 — deferred-decision (currently degraded — see the O-N6 note below) | votes, nominations, anything tallied at a decision point | monotone: well-formed and authored by a key authorized at the referenced log position. Never depends on revocation state. | computed at the decision point against the log prefix the decision pins. Deterministic function of converged state. | yes, within the open window — and the cost is small under this model, because the vote key would be keyed by the subject, so re-casting from a new device overwrites and the person loses nothing. This is not true today — see the note below |
| 2 — immediately settled | ledger entries, receipts, anything with irreversible external effect | authority checked once, at admission, and finalized | fixed at admission | no — prospective only. Bounded by scope, not by retroaction (R5.3). Honest analogue: a stolen card is stopped going forward, not unwound |
| 3 — authority events | the log's own events | ordered by the log itself | — | n/a — self-ordering |
The vote key is keyed by a
Did, not by a subject — verified.vote_key(proposal_id, voter: &Did)(apps/governance/src/state_store.rs:222-224) takes a key, which is concept 7 of §4's conflation table.save_vote(:379-384) is an unconditionalput,list_votes(:386-) scans the prefix, andAlreadyVoted(icn-governance/src/error.rs:33) is declared and never constructed. So today: re-casting under the same DID overwrites (#2469 fact 15), but a person acting under a second DID — which is what a second device means today (F19) — writes a second tallied row.That is the sybil surface subject/device separation exists to close, and it is a reason to prefer this model rather than evidence for it. (An earlier draft asserted the present-tense claim "votes are keyed by subject"; corrected in adversarial review.)
Bounding the window without a clock — and the mechanism as first specified does
not work. A device authorization carries validity expressed in log positions,
not wall-clock: valid for acts evaluated at positions in [N, N+k]. No clock is
read anywhere, which is the property worth keeping (R4.1; it also avoids F14's
fail-open entirely).
The window as originally written bounded nothing, and R5.1 is weaker than two earlier drafts claimed. (Refuted in review round 4.) The first draft checked the window against the position the act itself references. But the signer chooses that field: a compromised device simply keeps referencing
N, which is inside[N, N+k]forever, no matter how far the subject advances the log. Advancing does not age the device out; neither does revoking. A second draft narrowed the failure to sole-device compromise; that was also wrong — the hole is general.The window is only meaningful when checked against an evaluation position the signer does not control. That exists for exactly one act class:
Class Evaluation position Does the window bind? 1 — deferred-decision the position the decision pins Yes — the decision, not the signer, chooses it, so advancing past N+kdoes age the device out. Conditional on O-N6, which supplies the deterministic decision point2 — immediately settled none exists No. There is no decision point, so the only candidates are the acceptor's own current frontier — receiver-local, therefore divergent (R4.2/R4.3) — or freshness evidence the attacker controls Honest statement, replacing both earlier ones: R5.1 [HARD] is met for class 1 once O-N6 lands, and is not met at all for class 2. For class 2 a compromised device's authorization stays acceptable indefinitely; the only real controls are scope limits (a device may not settle above some bound) and a relying party's own freshness bar, which is the acceptor's risk decision under Rivest Proposition 1 and does not converge between acceptors.
Anchoring each act in the log — KERI's
ixn— would close it, and is rejected here for a stated reason: it makes every act an authority event, so two devices acting concurrently means two writers,k > 1, and consensus is required again (§6.1).The Let's Encrypt analogy is withdrawn: a certificate lifetime advances autonomously with time; a log position advances only if someone acts.
Cost, stated: a subject who advances too aggressively can strand an offline
device. Choosing k and the re-authorization cadence is OPEN (§17, O-N1).
Class 1 has a dependency this document does not discharge. "Evaluated at the decision point against the log prefix the decision pins" is deterministic only if the decision point itself is deterministic — i.e. if closing a proposal is a replicated act that fixes the pinned position, rather than a node-local handler action. ICN does not have that today: #2469 §7.2 keeps
ProposalClosedcontained precisely because "membership is not the right authority for choosing an outcome" andoutcome/tallyare attacker-supplied fields, and the correct authority "entangles with theGovernanceProofV2signer-authority model" that #2469 lists as a non-goal.So class 1 is convergent conditional on deterministic proposal closure, which is a governance problem with a known shape and a named owner — not an identity problem, and not one this document solves. Recorded as O-N6 (§17).
The fallback is not safe, and an earlier draft wrongly said it was. (Corrected in adversarial review.) Without a pinned position there are exactly two options, and both break a [HARD] requirement:
Fallback Breaks evaluate revocation against the receiver's local prefix at arrival R4.2/R4.3 — a node learning vote-then-revocation tallies differently from one learning revocation-then-vote. This is the arrival-order divergence #2469 §8 withdrew ignore revocation for class 1 entirely (consistent with "never depends on revocation state") R5.1 — a stolen device's votes count indefinitely This document picks the second, because divergence is a correctness failure while indefinite exposure is a bounded, stateable security cost — and because it degrades to exactly today's behaviour rather than introducing a new failure mode. It is a known R5.1 violation held open until O-N6 is answered, not a safe default, and it must be described that way wherever class 1 is claimed.
9.4 Governance authorship
Person holds continuity root (offline / backup)
│
├── subject S_A in Coop A, with its own authority log
│ │
│ └── log authorizes device D (scopes, positions [N, N+k])
│
▼
D signs the governance act, referencing log position N
│
▼
gateway / node RELAYS — holds no key of S_A, asserts no authorship
│
▼
receiver verifies, from bytes in hand + its own authenticated local state:
1. act signature under D's key [bytes]
2. D authorized at position N, scope admits the act [local log prefix]
3. S_A ∈ this domain's members [local domain state]
4. VALIDITY — and NOT against N, which the signer chooses:
class 1: the DECISION-PINNED evaluation position P ∈ [N, N+k]
class 2: no evaluation position exists ⇒ NO validity bound
(R5.1 not met; see §9.3)
── if the local prefix is shorter than N: QUARANTINE, never reject
Step 4 is written this way deliberately. An earlier revision read "position
Nwithin validity", which is the rule §9.3 retracts: the signer picksN, so a compromised device keeps referencing an in-range position forever. Round 4 fixed the narrative and left this normative flow stale — caught in round 5. Class 1 checks the position the decision pins; class 2 has no such position and therefore carries the explicit R5.1 shortfall rather than a bound that does not bind.
This satisfies #2469's I9 (no circular bootstrap — the log arrives on its own topic, not on the channel carrying the act) and R4.5 (local state that is authenticated and converges). It is the honest resolution of PRINCIPAL_MODEL §7.0.2's custody boundary: the gateway relays an act it cannot author, which is exactly what #2469 says is impossible today.
Quarantine must be bounded, and correctness rests on re-delivery — not on retention. "Never reject" invites an obvious attack: reference a log position that will never exist and the entry sits in quarantine forever. Rejecting unknown signing keys is not the fix — a device legitimately authorized at position
N+1will act before slow receivers holdN+1, and rejecting it would permanently discard a legitimate act, violating R4.6.The correct shape has two bounds, not one:
- Bound the position. Accept an act into quarantine only for
N ≤ (highest verified position) + Cfor a protocol constantC. Without this, an attacker cites position 2⁶³ and the receiver can never resolve it — it cannot distinguish "not yet replicated" from "will never exist", and §9.4 forbids fetching the log over the act's channel (I9). Alternatively, require the act to carry a hash-chain inclusion witness forN; carrying is not fetching, so R4.4/I9 permits it.- Bound the store. #2469 slice 4's bounded quarantine with eviction and a steward release valve, where an evicted entry is recovered by anti-entropy re-delivery once the prefix catches up.
Convergence therefore depends on re-delivery being reliable over time — a property of the replication layer, not of this design. Stating "never reject" without both bounds would be an unbounded-memory claim, and the position bound in particular was missing from an earlier draft (found in adversarial review). Folded into O-N5.
Note what is not claimed: this does not make the production gateway composition converge on its own. #2469 §7.0.1's second barrier — a receiver cannot deterministically evaluate standing/suspension because that state is unauthenticated (F17) — is untouched by anything here, and remains #2441's.
9.5 Where the Meaning Firewall falls
PRINCIPAL_MODEL §1.1 forbids Person in the kernel and §4 then proposes it as a
principal class. This model has no such tension:
| Layer | Holds | Never holds |
|---|---|---|
kernel (icn-kernel-api, icn-identity core) |
Did (a key), signature verification, canonical encodings, a generic attenuating delegation between principals |
any notion of person, member, device-class or coop |
identity (icn-identity) |
SubjectId, the authority log, pre-rotation |
why a subject exists, or what it is a subject of |
app (apps/*, governance, membership) |
Person, member, role, standing, recovery policy, act-class rules |
key custody |
A subject is meaning-free: it is any entity with an evolving authorized key
set. A person, a treasury and a node are all subjects. That is why this model
satisfies R13.1 where a Person principal class cannot.
One leak to watch. §9.1 says the log is "replicated to that subject's relying parties", and knowing which parties those are is context knowledge sitting inside the identity layer. Keep the identity layer's interface to an opaque destination set supplied from above, or R13.1 erodes here first. Flagged in adversarial review; folded into O-N5.
9.6 What this does not solve — read this before building on it
- The N1 per-subject authority-log primitive exists as additive library code in
icn-identity, but is not production-integrated. Its canonical bodies, durable join and pure derived view are implemented and tested; replication, gateway/runtime wiring and migration remain absent. The legacyRotationEventpath described in §3.7 remains unwired and is not replaced in production by N1. - N1 implements deterministic backward-chained pre-rotation as a library primitive, but no production path uses it. It remains load-bearing cryptographic machinery whose operational custody, replication and recovery integration are later slices.
- Selective disclosure does not exist and cannot be wired today. F16: blind signatures are not blind, the accumulator uses a 32-bit test modulus, the trusted-issuer check passes on an empty list, and the default build can neither prove nor verify. R6.2 and any nullifier-based sybil story are new cryptographic work, not integration.
- #2469 §7.0.1 still contains the production gateway composition. Nothing here lifts it.
- Guardian collusion and relay withholding remain unmitigated (scenarios 5, 11).
- Cross-context divergence under concurrent recovery is a real cost (scenario 3).
- The continuity root concentrates what Model A distributes — its compromise is takeover of every context plus full deanonymization, and under path (a) it is unrecoverable (§12.2).
- R9.1 [HARD] is not fully met. Path (a) fails if the backup is lost; path (b) needs threshold signing ICN does not have (F7) and which appears nowhere in §19.1's slice graph.
- Per-subject log availability is a real dependency (§7, Model C costs).
- Byzantine reliable broadcast is a prerequisite and does not exist — gossip entries carry no signature at all (§3.7, §6.1).
- Class 1 currently degrades to a known R5.1 violation until O-N6 (§9.3).
- Class 2 is not convergent under a fork — two replicas can hold permanently different settled state (O-N7). This model is not by itself a sufficient basis for irreversible settlement.
- A single compromised current key halts a subject unilaterally, and recovery orphans every later event (§9.2.1). Fork depth is a cost multiplier.
- Guardian recovery needs threshold signing (N7) to avoid being a kill
switch — independent M-of-N signatures make any
Mguardians able to halt the subject permanently (§12.2). - The durable set is attacker-writable; eviction must be protocol-specified and authority-aware, and beyond the frontier the gap region is unsolved — so R4.6 and permutation-invariant convergence hold only absent adversarial storage pressure (O-N8).
- Existing key-derived DIDs need a new
SubjectIdand a documented membership bridge — the DID cannot be the subject identifier (§15.1). - R5.1 [HARD] is not met for class 2 at all, and is met for class 1 only once O-N6 lands. The position window binds only when checked against an evaluation position the signer does not control, and class 2 has none (§9.3).
- R2.3 [HARD] is qualified — receipts from positions orphaned by a superseding rotation lose their authorization proof (O-N9).
- Threshold signing alone does not remove the guardian kill switch — closing it also needs enforced canonical body derivation and one next-authority commitment per position (§9.2.1 constraint 3).
10. What Did should mean
RECOMMENDED: option B of the mission's list — Did names a cryptographic
principal, and nothing else.
That is what the type already is (§3.1); the change is to stop projecting other concepts into it, and to make the type enforce what it claims.
| Change | Rationale | Classification |
|---|---|---|
Did = an Ed25519 (or successor) public key. No other construction. |
§3.1; F1 and F2 are the current alternative failing in production | RECOMMENDED |
Remove Did::from_anchor_id and new_unchecked |
Their only effect today is to mint values that ~50% of the time cannot be read back (F1) and that a ZK prover silently replaces with an all-zero key (F2) | RECOMMENDED |
| Canonicalize the encoding — pin base58btc at parse, or compare by decoded key bytes | Did equality is string equality (§3.1), so two encodings of one key are two unequal DIDs. Folds in PRINCIPAL_MODEL O10 |
RECOMMENDED |
Treasury identifiers become EntityId-shaped, not Did-shaped |
A treasury holds no key; derive_treasury_did mints a string that would fail Did::from_str and a second, different one via from_anchor_id (icn-coop/src/actor.rs:493) — already flagged in docs/status.toml:77 |
RECOMMENDED |
A durable subject is a SubjectId, never a Did |
§4's proof | ESTABLISHED (the proof), RECOMMENDED (the type) |
Should a global Person ID exist? No. Not because privacy outranks verifiability, but because nothing needs it: authorship is provable against context-local authenticated state (§9.4), and sybil resistance needs uniqueness, not identification (R6.6). Every mechanism whose sole purpose was to make a global identifier work — global current-root discovery, global fork selection, global genesis bootstrap — is cost with no corresponding requirement.
11. Privacy and correlation
11.1 What each observer learns
| Observer | Learns | Does not learn |
|---|---|---|
| unrelated network peer | that some key signed something on a topic | which subject, which person, which contexts |
| node operator / gateway | the subjects and device keys it relays for — including, in the common hosted-only case, several of one person's contexts and therefore their linkage | the continuity root |
| a cooperative | its own S_A, its authorized device keys, its acts |
S_B; that S_A and S_B are one human |
| a federation | the federation-scoped subject of a delegate | the delegate's coop-scoped subject, unless the coop discloses it |
| recovery guardian | that they are a guardian for one subject | the person's other contexts (if guardians are per-context) |
Device keys must be per-context too. If one device key appears in two contexts, the identifier separation is defeated. Per-context device keys are a deterministic derivation from the device's own secret.
The derivation must not take
SubjectIdas input for the initial key, or genesis is uncomputable. (Caught in review round 5.) The inception body contains the initial keyK₀, andSubjectId = event_id(inception body). So settingK₀ = KDF(secret, SubjectId)requires solving the fixed pointS = H(inception(KDF(secret, S), …))— offline genesis cannot construct it.The fix is to derive the initial key from a pre-subject context nonce chosen locally and carried in the inception body:
K₀ = KDF(secret, nonce). The nonce exists before the subject does, so there is no cycle. Keys for later positions may safely takeSubjectIdas input, because by then the subject exists.
That derivation is itself a correlation oracle, and an earlier draft called it "bookkeeping, not custody burden". (Corrected in adversarial review.) Anyone holding the device secret can recompute the per-context key for any candidate
SubjectIdand so confirm or deny membership in any context they can guess. Device-secret compromise therefore deanonymizes every context, not just the one the device was stolen for. A non-derived (independently random, stored) key per context removes the oracle at the cost of backup complexity. OPEN — folded into O-N3.
Residual correlation, stated honestly. Four channels, none of them closed:
- Within a context — everyone in Coop A correlates
S_A's activity there. Unavoidable and correct. - Shared infrastructure — a gateway relaying two of a person's contexts sees both. This is the default hosted deployment (R11.1), not an edge case.
- Recovery timing — recovery is a logged event visible to every relying party (R9.2). A person recovering after device loss rotates in every context within one window, and an observer of two coops' logs correlates on timing. R6.4 is weakened by the very auditability R9.2 requires — a genuine tension between two requirements, not an oversight.
- Social knowledge, timing and network metadata generally.
This is protocol-level unlinkability against a passive observer of distinct contexts, not anonymity, and nothing here should be described as the latter.
11.2 Sybil resistance without identification
SDIS's genuine contribution is VUI — "proves uniqueness without revealing the
underlying identity data". The correct shape is a per-context nullifier
derived from a uniqueness credential: an institution learns "this is one distinct
verified human, and not one it has already admitted" without learning which.
This cannot be built today (F16), and saying otherwise would be inventing a protocol to close a hole. Interim honest posture: uniqueness is a steward attestation against a registry the steward holds — which is what SDIS actually does — and that registry is a correlation surface. Recorded as OPEN (O-N2).
11.3 Proving two personas are one human
The person signs a linkage statement with the current authorized keys of both
subjects, naming both SubjectIds and a purpose. Cheap, consent-gated,
verifiable, and revealing nothing until the person chooses. This is the property
Model A gets backwards: there, linkage is always on and cannot be withheld.
12. Recovery
12.1 Pre-rotation is the recovery mechanism
The inception event commits to a digest of the next key set. If that next key set is derived from the continuity root, held in backup and not on any device, then total device loss is recovered by restoring the root, deriving the pre-committed keys, and rotating.
This dissolves O14's mechanism — and only its mechanism. O14 asks how a
total-loss recovery can advance the chain when RotationEvent::verify requires
an existing non-revoked authorized key (multi_device.rs:541-552) and total loss
has none. Pre-rotation answers exactly that: the committed-but-never-used next key
is an authorized key, so no separate "threshold-recovery transition" needs to be
bolted beside the chain rule — it is the chain rule.
What it does not solve, stated plainly. Pre-rotation presupposes the pre-rotation material survived. It converts "I lost my devices" into a solvable problem; it does not solve "I lost my devices and my backup." That case has only one answer — guardians (path (b) below) — and a person who chose path (a) and lost both has lost the subject. Describing O14 as fully closed would be exactly the invented-protocol failure the review discipline forbids.
12.2 Two paths; the person chooses
| (a) Self-recovery | (b) Guardian-gated | |
|---|---|---|
| Pre-rotation commits to | keys derived from the continuity root (recovery phrase) | a threshold key set held by M-of-N guardians |
| Total-loss recovery | restore phrase → rotate | M distinct guardians co-sign → rotate |
| Root/backup compromise | takeover, and unrecoverable — see below | insufficient alone |
| Any M guardians can... | — | halt the subject permanently. Closing this needs a single group key (N7) and authorized enforcing canonical body derivation and exactly one next authority per position — N7 alone is not sufficient; see below |
| Produced offline? | yes | no — needs an online M-of-N quorum |
| UX cost | one phrase at onboarding | guardian setup + coordination |
Path (b) is a kill switch, and a single group key is necessary but not sufficient to close it. With
M-of-Nguardians signing independently, two different quorums produce two different authorized establishment bodies at the same position, sosupersedecannot choose and the subject halts permanently (§9.2.1, constraint 3). That means anyMcolluding guardians can destroy the subject rather than merely capture it — a strictly worse outcome than takeover — and it can happen with no adversary at all, if a phrase-holder rotates atpwhile a guardian quorum recovers atp. Path (b) therefore requires a single group public key emitting one signature over one body (FROST-class), which makes N7 a hard prerequisite of guardian recovery rather than an optional upgrade. An earlier draft's "insufficient alone" understated this badly.And N7 alone still does not close it (round 5): a group key authenticates a quorum, it does not restrict that quorum to signing one message. Closing it needs the two rules in §9.2.1 constraint 3 —
authorizedmust enforce canonical body derivation, and each position must commit to exactly one next authority, so path (a) and path (b) can never both be armed at the same position.
Path (a) root compromise is not a race the victim can win. An attacker holding the continuity root derives the pre-committed keys, rotates first, and commits a fresh
nextdigest the victim does not hold. From that point the victim cannot rotate even though they still know the root — pre-rotation has handed the attacker the same one-way advantage it was meant to give the legitimate holder. Worse, the attacker can do this silently from a backup copy, at leisure, with no need to touch a device.So pre-rotation makes root compromise more decisive, not less. It is an excellent defence against device compromise and no defence at all against backup compromise. That asymmetry is the entire argument for path (b), and an earlier draft's matrix cell claiming "root compromise survivable ✔ pre-rotation" was simply wrong.
Default is (a), upgradeable to (b). That answers PRINCIPAL_MODEL's O1 ("may the root live on the first phone?"): the operational keys live on the phone; the pre-rotation material is backed up off-device at onboarding. R10.2 is met — phone-only onboarding is secure and recoverable by default rather than degraded — at the cost of exactly one irreversible user decision (R10.3).
12.3 Constraints any recovery must satisfy
- R9.4 — distinct authenticated participants. Today both threshold paths fail:
sync.rs:263-269neither verifies signatures nor deduplicates; SDIS'sapprove_by_steward()is a bare+= 1. Fixing this is #2591 plus its SDIS sibling, and it is a precondition for (b), not a follow-up. - R9.2 — recovery is a logged event. It is an ordinary event in the subject's authority log, visible to every relying party. Never a silent substitution.
- R9.6 — contestable, and this is a named exception to R4.1/R4.3. A relying
party may apply a notice-and-veto window before honouring a recovery rotation.
It cannot be position-denominated: in a takeover the attacker controls log
advancement and would simply advance past any positional window. So it is a
wall-clock window, and deciding whether to honour a rotation is a validity
decision.
Consequence, stated rather than finessed (found in adversarial review): two nodes of the same institution with identical durable state but skewed clocks can reach different verdicts during the window. That violates R4.1 and R4.3, both [HARD]. It is accepted here as a bounded, deliberate exception confined to recovery-rotation acceptance — never to ordinary act admission — because the alternative (no contest window) makes R9.6 unimplementable and hands a stolen backup an uncontested takeover. A deployment that cannot tolerate the exception must set the window to zero and accept that instead. This is the only place in the design where a receiver's clock decides anything.
- R9.5 and R2.4 — path (a) does not meet them, and that is a deliberate trade. The continuity root in path (a) is a permanent secret, and an attacker holding it can rotate the subject away. Recovery from that is a race the legitimate holder may lose, mitigated only by a relying party's notice-and-veto policy. Path (a) therefore trades R2.4/R9.5 [STRONG] against R10.2 [HARD] — phone-only onboarding that is recoverable by default. Path (b) removes the single-secret failure at the cost of guardian UX. The trade is the person's to make, and a deployment that silently pins everyone to (a) has made it for them.
- The recovery↔DID contradiction must be adjudicated (§3.6). Under this model
the subject identifier is unchanged by recovery — it commits to the
inception event, which recovery does not rewrite.
social-recovery-design.md'sdid_mapping:<old_did>indirection becomes unnecessary and should be retired.
13. Offline and partition semantics
| Operation | Offline? | Why it converges |
|---|---|---|
| Genesis (inception) | fully | self-addressing; nothing to contact (R7.1) |
| Signing an act | fully | references a log position the device already holds (R7.2) |
| Authorizing a device | fully | an event in the subject's own log |
| Recovery rotation, path (a) | fully to produce | log event |
| Recovery rotation, path (b) | no — needs an online M-of-N guardian quorum, and the R9.6 veto needs a notification channel bound to the subject, which total loss removes and takeover captures | §12.2 |
| Verifying an act | needs the log prefix | shorter prefix ⇒ quarantine, never reject (R4.6) |
| Two devices acting concurrently while partitioned | yes | acts are unordered; only the authority log is single-writer (R7.4) |
| Two devices authorizing concurrently | no — forbidden | k > 1 ⇒ consensus (§6.1). A correctness boundary |
No wall clock in act admission. Positions replace timestamps (§9.3), which also removes F14's fail-open failure mode. One validity decision does use a clock — recovery-rotation acceptance (§12.3) — and it is declared an exception rather than hidden.
Where clocks legitimately remain — and why they are not R4.1 violations. R4.1 forbids a receiver's clock from deciding validity. Three clock-shaped things survive, and none is that:
Thing Whose clock What it decides "the subject periodically advances the log" (§9.3) the subject's, on their own device when they re-authorize. A liveness schedule, not a verdict. Every receiver still decides positionally a relying party's notice-and-veto window before honouring a recovery (§12.3) the acceptor's whether to honour a recovery rotation — a genuine validity decision, and a named R4.1/R4.3 exception, not an exempt case. See §12.3 quarantine eviction (§9.4) the receiver's when to forget and rely on re-delivery — never whether an act is valid The test to apply to any future addition: if two honest receivers with identical durable state could reach different verdicts because their clocks differ, it is an R4.1 violation. The first and third above cannot. The second can, and is therefore declared an exception in §12.3 rather than defended as compliant.
14. Institutions and nodes
14.1 The pattern generalizes — and that is a result, not an aesthetic
durable subject ≠ current authority
holds for humans (subject ≠ device keys), for institutions (charter ≠ current stewards) and for nodes (instance ≠ operator). All three are subjects in the §9.1 sense, which is why the identity layer can stay meaning-free.
14.2 Institutions: PRINCIPAL_MODEL §4.1 is upheld, with a caveat
Its conclusion — an institution holds no signing key, because Did models custody
and institutions are constituted by governance — stands, and this model
strengthens the reasoning: Did is only a key here, so it is even more clearly
the wrong type for a governed entity.
But the caveat matters. The argument's premise was "an institution's identity would have to be a key." Under §9.1 that premise is false: an institution could be a subject with an authority log whose authorized keys are its current stewards, and no single person would "be" the institution. O12 therefore reopens on better terms — the question is no longer "key or mandate chain" but "does an institution need a self-authenticating statement, given that a subject log now makes one possible without single-custodian capture?" Recorded in §17.
FounderSignature (F18) is the existing primitive to build on: an institution's
inception event, signed by its founding persons, is a natural EntityId binding
and resolves PRINCIPAL_MODEL's O11 by construction (the id commits to the event).
14.3 Nodes
Node identity is the strongest binding ICN has — the Hello three-fact cert check
(icn-net/src/handlers/hello.rs:62-97) is genuinely sound, and is per
connection, not per peer (handlers/mod.rs:76-78). Nothing here changes it.
Two facts worth carrying forward: operator_did is populated as did.clone(), did.clone() (supervisor/lifecycle.rs:371-375), so node and operator are the
same principal today; and node key rotation produces a new DID and is never
persisted (keystore.rs:1224-1257 does not call save_v4). Under §9.1 a node
becomes a subject whose authority log survives key rotation — which is the
principled fix for both, and which also gives O2 (does a restored node keep
its DID?) a cleaner answer: the subject persists, the keys rotate, and
restore-twice is detectable as duplicity rather than indistinguishable from a clone.
15. Migration
Classification per mechanism: KEEP · MIGRATE · DEPRECATE · REMOVE · COMPATIBILITY-ONLY.
15.1 Key-derived Person DIDs — KEEP the key, allocate a new subject
This is the largest deployed class, and the key does not change. An existing
did:icn:<key> is already exactly what §10 says a Did should be: a
cryptographic principal. Two consequences:
- Nobody "becomes a new person" (scenario 25). No cutover, no re-enrollment, and no key ceremony: the person keeps signing with the key they already hold.
But the existing DID cannot itself serve as the
SubjectId, and an earlier draft said it could. (Corrected in review round 3.)SubjectId = event_id(inception body)and inception admission checksσ == event_id(b), whereasdid:icn:<key>encodes an Ed25519 public key. Barring an accidental preimage the two cannot coincide, so such an inception is inadmissible and the derived view staysUnknown.The honest migration is therefore: allocate a new
SubjectId, whose inception event names the person's existingDidas its initial authorized key. What is preserved is what actually matters — the key, custody, and the ability to sign — and what changes is the context identifier.That is a smaller cost than a re-enrollment and a larger one than "no event". In particular, membership rows keyed by the member's
Didneed a documented bridge to the newSubjectId, which is the same shape as the bridgeicn-commonsalready maintains for enrollment-DID → anchor (§15.2). Specifying that bridge is a migration prerequisite, not a detail.Rejected alternative: a "legacy inception" rule admitting
σ == did_of(initial_key). It preserves the identifier but destroys inception uniqueness — two different inception bodies could then claim oneσ, producing duplicity at position 0, which §9.2.1 relies on being impossible.
15.2 SDIS anchor-derived DIDs — DEPRECATE, at near-zero cost
PRINCIPAL_MODEL rates O18 critical on the assumption that anchor-derived Persons are a live identity class with deployed authority. Anchors are created in production (F4) — but two verified facts show they carry no authority:
- F5 — in the enrollment path the acting principal is already the device
key:
simple_enrollment.rs:667("The DID is the ephemeral_did — in SDIS, keys are created on the device"), and the token is minted forephemeral_did(:782). The anchor is returned alongside asanchor_id. - F20 — the anchor's mutating routes are disabled, returning
Forbidden.
And F4 shows the anchors that do exist carry none of the properties the design
claims for them: the VUI is SHA256("gateway-enrollment-vui:" ‖ did), so it is a
pure function of the public DID.
No deployed anchor-derived DID carries authority, and no deployed anchor carries a uniqueness or unlinkability property. Migration is therefore:
That trace is now complete — and it comes out clean, for an uncomfortable reason. (Left open in the first pass; resolved in review round 2.)
Step Finding PersonhoodAnchorStore::putindexes the anchor-derived DIDpersonhood_store.rs:202-205—let did = anchor.to_did(); … put(&did_index_key(&did), anchor_id)link_did, which would add the member's real DID to that indexzero production callers. The one hit, icn-net/src/rate_limit.rs:1781, sits after the#[cfg(test)]boundary at:1406PersonhoodAnchorStore::get_by_didreached only by the trait forwarder get_anchor_by_didatpersonhood_store.rs:599, and no production code constructs this store (supervisor/lifecycle.rs:1407passesNone). (An earlier draft said "zero production callers", which was imprecise.)| the live anchor-by-DID lookup is a different store |
icn-commons/src/store.rs:529, reached fromapi/commons/mod.rs:131,469— and it maintains an explicit bridge,put_anchor_did_index, doc-commented "used when the enrollment DID differs from the anchor's internal DID" (store.rs:539-545), written during enrollment aticn-commons/src/inner.rs:173| | standing / holder lookups | go throughCommonsStore::get_holder_by_did(holder_did), whereholder_didis the DID passed tocreate_holder_from_anchor— the member'sephemeral_didin the enrollment path (simple_enrollment.rs:703) |So there is no live cross-keying mismatch — and for a better reason than the first draft gave. The live path (
icn-commons) maintains an explicit bridge index from the enrollment DID to the anchor, precisely because the two differ; the path that indexes only the anchor DID (icn-identity'sPersonhoodAnchorStore) is never constructed in production. Standing and membership consistently key on the member/device DID.This does not make the migration risk-free, and the finding should not be read as a clean bill of health. It means the hazard is latent rather than active: the first caller to use
PersonhoodAnchorStore::get_by_didwith a member DID getsNone, because only the anchor DID was ever indexed andlink_didis never called. That fails closed, which is the safe direction, but it is surprising and would read as "this person has no anchor". Removingto_did()should therefore also remove or repurpose the DID index rather than leave a half-wired lookup behind.
| Mechanism | Disposition |
|---|---|
Anchor as a record |
KEEP — it is a durable subject label, which is a real need |
Anchor::to_did / Did::from_anchor_id |
REMOVE — the source of F1 and F2 |
| anchor id in API responses | COMPATIBILITY-ONLY, then removed — it is currently returned twice, as anchor_id and as did (api/commons/anchor.rs:67) |
is_anchor_did |
REMOVE — vacuous (F11) |
VUI / steward network |
KEEP the concept, MIGRATE the wiring — uniqueness is a real requirement (R6.6); compute_temporary_vui = SHA256(did) (F6) must go, and combine_prf_partials is not a threshold scheme (F7) |
15.3 Everything else
| Mechanism | Disposition | Note |
|---|---|---|
DidDocument (multi_device.rs) |
MIGRATE → authority log | Cheaper than it looks: the gateway path never creates one (F8), so only the icnctl/keystore path has state |
DidDocument (icn-kernel-api) |
REMOVE | zero implementors; a parallel unimplemented sketch over pub type Did = String |
RotationEvent |
MIGRATE | right shape; needs a domain-separated canonical preimage (F12) and a signer/verifier that agree (F10) |
| device rosters | MIGRATE | fix #2588 (binding) and #2590 (attenuation) as part of the migration, never after |
| memberships | MIGRATE (was "KEEP, extended") | the bidirectional index (sled_registry.rs:9-12) is the right shape, but rows keyed by the member's Did need a documented bridge to the new SubjectId (§15.1). Signing and replication remain #2441's (F17) |
| governance state | KEEP | class 1 becomes cheap once votes key on the subject; today vote_key(proposal_id, voter: &Did) (apps/governance/src/state_store.rs:222) keys on a Did, so re-casting from a new device writes a second tallied row (§9.3) |
| historical signatures / receipts | KEEP — the signature is permanently verifiable; the authorization is not | Pre-fork positions: "key K was authorized at position N" is an append-only fact and R2.3 holds. After a superseding rotation at p, a key first authorized at N > p is no longer established by the derived view — the receipt still proves "K signed these bytes", and with the orphaned bodies retained "this body existed on a branch later superseded", but not that K remained authorized (§9.2.2). Migration must therefore keep the orphaned bodies and their chaining, not the raw receipts alone; full historical authorization needs the per-branch endorsement evidence O-N9 leaves undefined |
| gateway challenge auth | MIGRATE | add a domain separator and bind coop_id/scopes into the signed payload (§3.4) |
| invite redemption | REMOVE the unproven-subject mint | #2589 |
| React Native wallet | MIGRATE | today one key per install is the person (F19); needs subject/device separation and off-device pre-rotation backup |
| personal nodes | KEEP | §14.3 |
GOV_OP_V1 |
DEPRECATE per subject | see O13 in §18 |
16. Rejected alternatives
| Rejected | Reason |
|---|---|
| Key-derived durable Person identifier (Model A; PRINCIPAL_MODEL §4 Person row) | Entails a permanent, unretireable master key (§5); violates R2.4/R9.5; generates O8/O13/O16/O17/O18 |
| Globally resolvable non-key anchor (Model B) | Genesis has no authority root; resolution needs a global directory with an unsolved split-view problem; correlation worse than A |
| Credential-only, no subject identifier (Model D) | Collapses into C plus a credential layer; revocation reintroduces an online correlator; no continuity mechanism |
| Hybrid five-concept graph (Model E) | Complexity is the cost; §3 documents what ICN's existing parallel identity models already produce |
| O9 as posed — a convergent global device-revocation model | Unachievable within premises ICN has chosen (§6.1) — a bounded argument, not a theorem. Replaced by §9.3 |
Wall-clock validity windows (not_after as an ingress gate) |
Non-convergent (#2469); and F14 shows ICN's clock helper fails open |
standing_hash in the envelope |
#2469 §14.1 — a deterministic-looking check over unauthenticated state |
| Carried-proof-only verification ("carried, not resolved" as an absolute) | Over-generalizes #2469, which resolves against local durable state by design. Corrected in R4.5 |
| Institution DID held by threshold signing (A′) | ICN has a threshold PRF, not threshold signing (F7); requires FROST-class new work. Not rejected forever — see O12 in §17 |
| ZK selective disclosure as a near-term mechanism | F16 — the stack cannot prove or verify in its default build |
17. Open decisions
Carried forward from PRINCIPAL_MODEL where still live, plus new ones from this pass. A correct OPEN is better than an invented answer.
| # | Question | Why open |
|---|---|---|
| O-N1 | What are the device-authorization validity span k and the re-authorization cadence (§9.3)? |
Too small strands offline devices; too large widens the compromise window. Needs empirical UX input, not a derivation. Does not bound sole-device compromise at all (§9.3) |
| O-N2 | How is per-context sybil resistance built, given F16? | A nullifier needs working ZK. Interim is steward attestation against a registry that is itself a correlation surface (§11.2) |
| O-N3 | Are recovery guardians per-context or shared? And are per-context device keys derived or independently random? | Shared guardians are better UX and worse privacy (the guardian learns the linkage); per-context is the reverse (R6.4). Derived device keys are a confirm-or-deny oracle on context membership for anyone holding the device secret (§11.1); independent keys remove the oracle and complicate backup |
| O-N4 | Which act classes exist beyond the three in §9.3, and who assigns a new act to a class? | §9.3 is exhaustive today but the taxonomy must be governed, or class assignment becomes an unreviewed security decision |
| O-N5 | How is the authority log replicated and bounded? | It is per-subject and monotone, so gossip suffices in principle — but topic naming, retention, and a bound on log growth are unspecified, and ICN's topics are global-by-domain today (§3.7). Topic naming is also a correlation surface (§11.1) |
| O-N6 | What makes a governance decision point deterministic, so §9.3 class 1 can pin a log position? | #2469 §7.2 contains ProposalClosed because outcome/tally are attacker-supplied and the real authority entangles with the GovernanceProofV2 signer model, an explicit #2469 non-goal. A governance problem, not an identity one — but class 1 degrades to class 2 until it is answered |
| O16′ | On detected duplicity, what happens beyond refusal — and what bounds the DoS? | Reclassified in review round 2 from a liveness question to a settlement-safety one. A single compromised current key halts a subject unilaterally; recovery orphans the suffix; and if device compromise persists, each revealed pre-rotation generation is captured in turn. KERI concedes "an unavoidable race condition" |
| O-N7 | What makes class-2 (irreversibly settled) acts safe under a fork? | New in review round 2. Live(branch A) → Live(branch B) leaves two replicas with permanently different ledger state, because settled effects are never unwound (R5.3). Reducing exposure needs a finality depth, which ICN does not have and whose local evaluation reintroduces the divergence. Until answered, this authority model is not by itself a sufficient basis for irreversible settlement (§9.2.1) |
| O-N9 | How is historical authorization evidence preserved across a superseding rotation? | New in review round 4. Orphaning the suffix means a receipt signed by a key first authorized after the fork keeps a verifying signature but loses its authorization proof, degrading R2.3 to "authored on a superseded branch". Preserving it needs durable per-branch endorsement records this model does not define (§9.2.1) |
| O-N8 | What is the protocol-specified admission and compaction rule for the durable event set? | New in review round 2, widened in round 3. admissible is cheap, so the set is attacker-writable and grow-only; eviction is forced, and if implementation-chosen then "convergence over the eventual retained set" is vacuous under attack. Authority-aware eviction (retain authorized before unauthorized) handles spam at or below the frontier; the gap region beyond the frontier is unsolved, so R4.6 and permutation-invariant convergence hold only absent adversarial storage pressure (§9.2.1) |
| O12 | Do institutions need self-authenticating statements — now that §14.2 makes one possible without single-custodian capture? | Reopened on better terms. The original rejection assumed institution-identity ⇒ institution-keypair; §9.1 falsifies that premise |
| O2 | Does a restored node keep its identifier? | §14.3 improves it (subject persists, keys rotate) but does not settle restore-twice detection policy |
| O5 | Remove or wire public_did institutional signing? |
Depends on O12 |
| O6 | Does the membership-credential layer belong in this arc? | Needed for portable standing (Model D's genuine contribution); not needed for §9.4 |
| O11 | Canonical encoding of a genesis decision before hashing to an EntityId? |
§14.2 makes this the same mechanism as SubjectId, so it should be solved once, not twice |
| O15 | Canonical preimage of the node-claim transcript | Unchanged from PRINCIPAL_MODEL §5.3 |
Closed by this document: O1 (§12.2); O7 (§9.3's class table answers "which operation classes may a device sign"); O8 (§18); O9 (§18 — rejected as posed, which is a disposition, not a solution).
Decided in principle, mechanism still open: O3 — pairwise identity is adopted with costs stated (§11), but the mechanism that would complete it is O-N2, and F16 says it cannot be built today. Calling O3 "closed" would be relabelling.
NOT closed, contrary to an earlier draft of this list (corrected in adversarial review, which caught it contradicting §18): O10 — still required and widened; O13 — partial, receiver-side retirement unsolved; O14 — mechanism only, custody remains; O16 — narrowed to O16′; O17 — partial, authenticity ≠ acquisition; O18 — closed as an identity class, but see the untraced anchor↔member keying question in §15.2.
18. Consequences for the recorded open decisions and slices
| Item | Disposition |
|---|---|
| O8 — current root after rotation | DISSOLVED. Replay the subject's log from a self-certifying inception (§6.2, §9.2). No external authority, no clock |
| O9 — convergent device revocation | REJECTED AS POSED — unachievable within the premises ICN has declined to pay for (§6.1); not a theorem-level impossibility. REPLACED by §9.3's per-act-class semantics plus position-bounded validity, which itself carries the O-N6 and O-N7 gaps |
O10 — canonical DeviceAuthorization bytes |
STILL REQUIRED, and widened: it now covers the log event preimage (F12: bincode, no domain separator) and the Did canonicalization rule (§10) |
O13 — retiring GOV_OP_V1 |
PARTIAL, and an earlier draft had this backwards. O13 constrains receivers, not authors. A subject writing "v1 no longer accepted for me" binds only acts they author; any subject who never writes it keeps v1 alive forever — which is precisely the never-ending dual stack R12.3 [HARD] forbids. Per-subject declaration is a genuine improvement on a global cutover, and receiver-side retirement remains unsolved |
| O14 — total-loss recovery cannot advance the chain | MECHANISM DISSOLVED by pre-rotation (§12.1) — the committed-but-unused next key is the missing authorized key. The custody question remains: pre-rotation presupposes the backup survived, and "lost devices and backup" is answered only by guardians |
| O16 — fork selection under genesis compromise | NARROWED, not defused. Pre-rotation covers rotations only; a current-key compromise still forks a non-establishment event (§6.2). Recovery is by superseding rotation, which is racy. O16′ is now a liveness question, not only a selection one |
| O17 — authenticated genesis document | PARTIAL. Self-addressing proves the inception event is genuine; it does not prove the prefix you were handed is complete. A truncated-but-valid prefix verifies perfectly and yields a stale authority state — the split-view problem Model B was rejected for. KERI covers this with witnesses and watchers, which this model does not adopt. Availability/completeness is open (§7 Model C costs, O-N5) |
| O18 — anchor-derived Persons | DISSOLVED, at far lower cost than estimated (§15.2, F4/F5/F20) |
| Slice A0 (identity-document chain) | REDIRECTED, not deleted. Becomes the per-subject authority log with self-addressing inception and pre-rotation. Its hard prerequisites change: O17 and O14 are answered by construction; O16 shrinks; and it gains one new requirement — pre-rotation — that did not exist before |
| Slice A (device principal + authorization) | UNBLOCKED in principle, because O8 and O9 no longer gate it — but its field set now depends on O-N1 (validity span) and O10 (canonical bytes) |
| Slice B (device enrolment) | KEEP, with #2588 + #2590 folded in. F8 shows the path is unreachable today, so this is a "fix before wiring" job, not a live-incident response |
| Slice C (mobile genesis) | KEEP, widened — must now also produce a subject and off-device pre-rotation backup (F19, §12.2) |
Slice D (GOV_OP_V2) |
UNBLOCKED from O9/O13, still bounded by #2469 §7.0.1: the receiver cannot evaluate standing/suspension because that state is unauthenticated (F17). That barrier is #2441's, not this document's |
| Slices E/F/G (node claim, institution genesis, hosting) | KEEP. §14.2 makes F's EntityId binding the same mechanism as SubjectId — solve once |
| #2469 slices 4–6 | Still independent. Nothing here blocks them |
19. Revised dependency graph and the next slice
19.1 The graph
N1 authority-log primitive (library only; no wiring)
│ SubjectId = H(inception) · pre-rotation · +1 monotone
│ canonical domain-separated preimage [absorbs O10, F12]
│
├──► N2 narrow `Did` [F1, F2 — removes from_anchor_id]
│
├──► N3 log replication == BUILD BYZANTINE RELIABLE BROADCAST
│ gossip entries carry no signature at all (F17/§3.7), so this is
│ a new primitive, not a new topic. + bounded quarantine [O-N5]
│ │
│ └──► N4 device authorization + attenuation
│ [folds #2588, #2590; needs O-N1]
│ │
│ └──► N5 GOV_OP_V2 member-origin signing
│ [still bounded by #2469 §7.0.1]
│
├──► N6 mobile: subject + device split + pre-rotation backup [F19]
│
└──► N7 threshold SIGNING (FROST-class) — a HARD PREREQUISITE of guardian
recovery, not an upgrade. ICN has a threshold PRF, not threshold
signing (F7). Without N7, independent M-of-N signatures give any
M guardians a KILL SWITCH (two quorums => two authorized bodies at
one position => permanent halt), so path (b) must not ship first.
NECESSARY BUT NOT SUFFICIENT: N7 alone leaves the halt attack
intact — a group key authenticates a quorum, it does not bind that
quorum to ONE message. Path (b) is safe only with N7 PLUS
`authorized` ENFORCING canonical body derivation PLUS exactly ONE
next authority armed per position (§9.2.1 constraint 3).
Without N7, R9.1 [HARD] is met only by the backup-phrase path.
independent, and should not wait:
X1 #2589 invite-mint fix
X2 #2591 recovery signature + dedup (+ the SDIS sibling)
X3 the F2 / F13 findings — see §19.3
The old order — [O18] → A0 → (B ∥ C) → [A blocked on O9; D blocked on O9+O13] → …
— is replaced. O18 no longer precedes anything (§15.2), and A/D are no longer
blocked on O9 because O9 is not a prerequisite; it was an unachievable goal.
19.2 The exact next slice — N1: the authority-log primitive
Smallest slice that either validates or refutes the architecture, with no
wiring, no gateway change, no SignedGovernanceOp change.
Invariant — stated over §9.2.1's two-layer model, not over a "verified prefix". (The earlier formulation — permutation-invariance of
(longest verified prefix, duplicity set)— is withdrawn; §9.2.1 explains why it is not a join.) Two obligations, tested separately:- Durable layer.
S(σ)is a set ofadmissibleevents keyed byevent_id = H(canonical_body); the join is set union;admissibleis state-independent, so it commutes with union. - Derived layer.
derive(S(σ))is a pure function — same set, same verdict, regardless of receipt order — halting on|C| ≥ 2aftersupersede.
- Durable layer.
Seam. A new module beside
icn/crates/icn-identity/src/multi_device.rs. Do not modifymulti_device.rsin this slice.Proof / tests — the two layers are tested separately.
Durable join (properties, ideally
proptest):- commutativity —
S₁ ⊔ S₂ = S₂ ⊔ S₁; - associativity —
(S₁ ⊔ S₂) ⊔ S₃ = S₁ ⊔ (S₂ ⊔ S₃); - idempotence —
S ⊔ S = S, including a re-signed identical body, which must collapse to one element (theevent_idrule); - permutation-invariant durable state — every delivery permutation of one
event multiset yields the same
S; admissibleis state-independent — as a differential property, not a signature observation. Classify one event under many randomly generated surrounding contexts and require an identical verdict every time. (An earlier draft said "assert by construction: the function takes only the event"; that is unfalsifiable —fn admissible(&self, e: &Event)on a struct holding the store satisfies it while consulting state.)
Derived view (determinism): 6. deterministic authority view —
deriveover equalSyields equal verdicts, across permutations and across independent replica instances; 7. gaps buffer — an out-of-order event is retained and unused, never rejected, and never wedges once the gap fills (cases 1–2); 8. conflicting same-position events →Halted(p), identically on every replica, in both arrival orders (cases 3–5); 9. unauthorized or malformed events cannot fork or block — spam at positionpnever entersC(case 7); 10. pre-rotation — a rotation revealing the committed material succeeds; one without it is retained but never authorized (cases 8, and scenario 1); 11. non-establishment fork detection — a compromised current key forking anauthorize/revokeis detected, not prevented by pre-rotation (case 9); 12. superseding recovery — a legitimaterotateat the disputed position supersedes the forked non-establishment event and advances (case 10). Two authorized establishment events at one position must halt, not choose; 13. no branch selection by content — as two falsifiable properties. (a)derive's verdict is invariant under any injective relabeling ofevent_ids, which no lowest-hash or ordering-based tiebreak can survive; (b) for every generated set with|C| ≥ 2aftersupersede,derivereturnsHalted. (An earlier draft asserted "no such rule exists anywhere", an absence claim over an unbounded space that nothing can fail.)Cases the first draft of this list could not fail — added after review round 2: 14. re-signed identical body collapses to one element and does not produce
Halted; the witness set grows instead; 15. deterministic rotation body — deriving the same rotation twice from the same state yields oneevent_id; a non-deterministic construction that yields two authorized rotations at one position is rejected at construction time, because atderivetime it is already an unrecoverable halt; 16. digest consistency —σ,prev_digestand dedup all useevent_id = H(canonical_body); a child naming a whole-event digest fails; 17. fork behind the frontier — bodies at 0–5 and 8–9 with a fork at 3 must halt at 3, and a superseding rotation at 3 must be shown to orphan 4, 5, 8, 9 (the test asserts the loss, so it cannot be discovered later as a surprise); 18. unilateral self-equivocation — one authorized key emitting two distinct bodies at one position halts the subject with no second party involved; 19. authority principals are rawDids — an event naming aSubjectIdas a device or guardian is rejected, soderivecannot become mutually recursive.Encoding and hygiene: 20. encode/decode round trip over a versioned, domain-separated, length-prefixed preimage, plus cross-implementation vectors (absorbs O10; avoids F12's bare-bincode signing domain); 21. no wall clock is read anywhere in the module — assert in CI, given F14.
- commutativity —
Security properties. Compromise of a current key cannot rotate, and can fork only non-establishment events — which superseding rotation recovers. Compromise of the inception key after a legitimate rotation grants nothing. Compromise of the pre-rotation material is out of scope of this slice's guarantees (§12.2).
Non-goals. No replication, no gossip topic, no device-authorization format, no revocation policy, no
Didchange, no migration.Dependencies. None. This is why it is first.
Migration impact. None — additive library code.
19.3 Findings that warrant issues before any of this
Listed, not filed — filing is a separate decision for the maintainer. The tracking column reflects a GitHub issue search performed at review round 2; it is a point-in-time result, not a standing guarantee.
| Finding | Why it should not wait | Already tracked? |
|---|---|---|
F2 — icn-zkp/src/prover.rs:227-230 substitutes [0u8;32] for a failed issuer key and proceeds |
A silent all-zero-key fallback in a prover. Independent of everything here | UNTRACKED — no issue matches icn-zkp, prover, or the fallback |
F13/F13a — RPC recovery lets a caller name an arbitrary victim old_did with threshold: 1, and the node signs the trustee attestation |
Latent critical: inert only because nothing applies recovery events (F9) | UNTRACKED. #2591 is the nearest and explicitly scopes to sync.rs/recovery.rs verification and dedup — it does not cover the RPC handler path |
F6 — compute_temporary_vui(did) = SHA256(did) on the routed enrollment path, and the second site at icn-commons/src/inner.rs:81-87 |
Destroys the VUI's stated unlinkability; the file itself says "NO sybil resistance" | UNTRACKED — #2468 covers enrollment retry idempotency, not VUI derivation |
| F1 — ~50% of hash-derived DIDs fail Ed25519 deserialization | Data-integrity defect masked by hand-picked test fixtures | UNTRACKED |
F14 — current_timestamp_secs() returns 0 on clock error |
Every expiry check in the identity stack fails open | HISTORICALLY TRACKED, NOT COVERED. #417 (the defect) and #425 (migrate security-critical callers to try_*) are both CLOSED — yet at b26bf681 ten files across icn-identity/icn-steward still call current_timestamp_secs() and zero call try_current_timestamp_*. The identity stack was never migrated |
| F8 — the gateway multi-device path is unreachable | Not a defect; a truth correction — docs/status.toml and the PRINCIPAL_MODEL gap matrix rate this capability as implemented |
n/a — a truth-plane correction, not a defect |
AlreadyVoted is declared and never constructed (icn-governance/src/error.rs:33), while save_vote is an unconditional put keyed by a Did |
Not new behaviour (#2469 fact 15 records the overwrite), but the sybil consequence — one human with two device DIDs writes two tallied rows — is worth its own decision | UNTRACKED as a sybil concern |
F20 live-state — /v1/sdis/anchor write routes now return Forbidden |
Not a new defect | #2448 is OPEN and partially stale: verified at b34cd3f6, its write attack is no longer reachable at b26bf681; the unauthenticated read half stands |
19.4 Entry conditions for the N1 session
N1 is a design-and-test slice, not a wiring task. Before writing code it must settle three things this document deliberately left open, because each changes the event field set and freezing the format first would repeat the mistake PRINCIPAL_MODEL §15.1 identified:
- The canonical preimage — version, domain separator, field order, encoding,
and the
Didcanonicalization rule (§10). Absorbs O10; must not repeat F12's bare-bincode signing domain. - The pre-rotation commitment scheme — what is committed, and how a rotation
proves it. This is the load-bearing new mechanism and has no precedent in the
repo. It must additionally make the whole rotation body re-derivable (e.g.
K_{p+1} = KDF(root ‖ p+1)with every other field determined), because pre-rotation commits to the next key set and not to the body — so a non-deterministic construction lets one honest signer author two authorized rotations at one position and halt the subject permanently, with no attacker present (§9.2.1, constraint 2).authorizedmust ENFORCE the canonical derivation, not merely permit it, and each position must commit to exactly one next authority — otherwise two quorums, or a phrase-holder racing a guardian quorum, still produce two authorized bodies (§9.2.1, constraint 3). - The event taxonomy — inception / rotate / authorize / revoke / recover;
which are establishment events (and so eligible for
supersede); and which may carry a validity span (O-N1). Two invariants are normative, not stylistic: an event is establishment iffauthorizedrequires a pre-rotation pre-image reveal — never a capability flag, and no delegation path may mint one, which explicitly bars §9.5's generic attenuating delegation from establishment authority; and every authority principal named inside an event is a rawDid, never aSubjectId, orderivestops being a pure function. - The replicated state model must be taken from §9.2.1, not re-invented. This
was added after review round 2 found that two successive drafts had specified a
merge that does not form a join. The N1 session inherits, and must not redesign:
- durable state = a set of
admissibleevents keyed byevent_id = H(canonical_body), joined by union; admissibleis state-independent — it must take the event and nothing else;- the derived authority view is a pure function of that set;
supersede(establishment over non-establishment) is the only selection rule, and|C| ≥ 2after it halts;- no content-based tiebreak of any kind — no lowest hash, no first-seen, no longest chain.
- durable state = a set of
Everything else in §19.2 is testable once those four are fixed.
20. Decision classification index
| Result | Class |
|---|---|
| A durable human subject identifier must not be a public key (§4) | ESTABLISHED (proof from R1.2/R2.1/R2.2, exhibited in multi_device.rs:21-22) |
Did is a key identifier; every non-key use is unsupported type reuse (§3.1) |
ESTABLISHED (F1, F2, F11) |
| ICN has no replicated authenticated ordered log a governance op lands on (§3.7) | ESTABLISHED (source-traced) |
| Global convergent revocation is unachievable within premises ICN has chosen (§6.1) | RECOMMENDED — a bounded argument, not ESTABLISHED. FLP forbids only deterministic consensus in pure asynchrony, and is circumvented by randomization, partial synchrony and failure detectors; Chandra–Toueg is itself a positive result. (Downgraded from ESTABLISHED in adversarial review.) |
| A single-writer authority log needs no consensus, given Byzantine reliable broadcast (§6.1) | ESTABLISHED (Guerraoui et al., PODC 2019) — with the caveat that BRB does not prevent a Byzantine writer equivocating, which is why §9.2 handles duplicity explicitly, and that ICN has no BRB today |
| Recency is the acceptor's decision (§6.3) | ESTABLISHED (Rivest, FC'98) |
| No deployed system achieves decentralized global revocation convergence (§6.1) | ESTABLISHED (survey of primary specs) |
| Model C — context-scoped identity with a private continuity root (§9) | RECOMMENDED |
| Durable state = two grow-only sets whose elements are canonical bodies (signatures held separately and never compared), joined by union (§9.2.1) | ESTABLISHED — join laws follow from set union once admissible is state-independent |
admissible must be state-independent, or the filter does not commute with union (§9.2.1) |
ESTABLISHED |
Derived authority view as a pure function; halt on |C| ≥ 2 (§9.2.1) |
RECOMMENDED |
supersede: establishment over non-establishment, with establishment eligibility defined by pre-committed material and never by a capability flag (§9.2.1) |
RECOMMENDED — the constraint is load-bearing; violating it makes the selector a privilege-escalation path |
| Class-2 settlement is not convergent under a fork (§9.2.1) | OPEN — O-N7. Not merely late detection: Live(A) → Live(B) leaves permanently different settled state. This model is not a sufficient basis for irreversible settlement until answered |
| Rotation bodies must be deterministic; threshold recovery must emit one body (§9.2.1) | RECOMMENDED, normative for N1 — without them a crash-retry or a second quorum halts the subject with no attacker |
Authority principals inside events must be raw Dids, never SubjectIds (§9.2.1) |
RECOMMENDED, normative for N1 — otherwise derive is not a pure function |
| Superseding recovery orphans the suffix (§9.2.1) | ESTABLISHED consequence — a stated cost of recovery, growing with fork depth |
| N7 (threshold signing) is a hard prerequisite of guardian recovery (§12.2) | RECOMMENDED — necessary, not sufficient. Without it any M guardians hold a kill switch; with it the switch closes only alongside enforced canonical body derivation and exactly one next-authority commitment per position (§9.2.1 constraint 3) |
| Fork monitoring is a client obligation ("responsive controller") (§9.2.2) | RECOMMENDED, inherited from KERI |
Did narrowed to "cryptographic principal" (§10) |
RECOMMENDED |
| No global Person identifier (§10) | RECOMMENDED |
| Per-act-class revocation semantics (§9.3) | RECOMMENDED |
| Pre-rotation as the recovery mechanism (§12.1) | RECOMMENDED — solves O14's mechanism; makes backup compromise more decisive, not less (§12.2) |
| Per-context subjects and per-context device keys (§11.1) | RECOMMENDED |
| Institutions hold no signing key (§14.2) | RECOMMENDED, with O12 reopened |
Anchors retained as labels; to_did removed (§15.2) |
RECOMMENDED |
| Key-derived Person DIDs unchanged (§15.1) | RECOMMENDED |
| O-N1…O-N6, O16′, O12, O2, O5, O6, O10, O11, O13 (receiver-side), O15, O17 (acquisition) (§17) | OPEN |
| R9.1 [HARD] total-loss recovery; R5.1 [HARD] not met for class 2 at all, and for class 1 only once O-N6 lands; R2.3 [HARD] qualified — orphaned positions lose their authorization proof; R4.1/R4.3 during the recovery veto window | ADMITTED SHORTFALLS — named in §9.6, not discharged |
Anchor-derived Dids; new_unchecked; is_anchor_did; kernel-api DidDocument; did_mapping recovery indirection |
LEGACY — compatibility only, then removed |
Model A key-as-person; Model B global anchor; Model D credential-only; Model E hybrid; O9 as posed; wall-clock validity; standing_hash; carried-proof-only as an absolute; threshold-held institution DID (for now); near-term ZK selective disclosure |
REJECTED (§16) |