IAM is an open, deterministic protocol for representing identity as a structured graph of entities, attributes, and trust relationships. It defines a minimal, implementation‑agnostic model for how identity data is expressed, linked, validated, and reasoned about across systems, without prescribing any runtime, product architecture, or operational workflow. IAM provides a stable semantic foundation that any platform, service, or application can adopt to ensure consistent interpretation of identity and trust across heterogeneous environments.
Modern identity systems confuse access with identity. Access is a runtime concern: login flows, federation, availability. Identity is not. Identity is a statement about existence and continuity. Any system that requires live connectivity or institutional availability to answer “who is this?” has already failed.
Boring because stable. The moment identity becomes dynamic or adaptive, it becomes policy or product. True identity does not negotiate; it records. Boring systems last: cryptographic hashes, append-only logs, DNS. Identity belongs to this class.
Deterministic because truth cannot depend on infrastructure. Same cryptographic material, same records, same result. Always. If identity changes depending on which resolver, network, or authority you ask, it is not a property of the subject but a side effect of the environment. Determinism is the only defense against silent mutation and reinterpretation.
Offline because identity must outlive systems. Networks fail. Organizations dissolve. Software rots. An identity that cannot be evaluated from archived data, without contacting any external service, is not an identity. It is a lease. History does not ask permission to be verified.
Most identity architectures solve the wrong problem: how to authenticate users today, federate trust between institutions, or monetize access. Valid problems, but not identity. Identity is the substrate beneath all of them. It must remain valid when none of them exist.
Below is the complete IAM protocol specification:
m=670625. iam-core 1.0.
Identity and trust graph. The constitution of JAM.
The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as described in RFC 2119 and RFC 8174.
IAM defines how identities are created, how trust structures are formed, and how records are signed, ordered, and validated. It does not define messaging, memory, models, storage, transport, inactivity detection, succession, or network topology. Those belong to the JAM runtime layer that builds upon IAM.
IAM is small on purpose. It contains only explicit transitions over a stream of signed records. All derived states (inactivity, succession, failure recovery) are runtime interpretation.
The trust structure formed by ACCEPT records is a directed acyclic graph (DAG), not strictly a tree. A member MAY have multiple parents, which means multiple independent lineages to the root. This is both simpler and more honest: a person can be vouched for by more than one existing member.
If this specification is correct, everything above it can change without breaking the core.
name username. lowercase ASCII slug. chosen once. never changes.
part of key derivation.
incantation pass phrase. secret. never stored. never transmitted.
nick display label. freeform. mutable. cosmetic only.
seal four-word visual fingerprint of the public key.
A user MUST remember two things: name and incantation. The seal is derived and shown for visual confirmation. The nick is decoration and plays no role in identity.
The canonical identity of a member is its Ed25519 public key. The seal is a human-readable fingerprint, not an identifier. Implementations MUST use the public key for all identity comparisons and MUST NOT rely on the seal for uniqueness.
Seal collisions are possible by the birthday paradox at approximately 77000 identities in a single graph. When two seals collide, implementations MUST disambiguate by displaying a short public key suffix.
A name MUST match the regular expression ^[a-z0-9][a-z0-9._-]{0,31}$.
Lowercase ASCII letters, digits, period, underscore, and hyphen. First character MUST be a letter or digit. Maximum length is 32 characters.
Implementations MUST reject any other input before key derivation. No Unicode normalization, no case folding, no whitespace trimming. What the user typed is either a valid name or an error.
Two different identities MAY share the same name slug. Such identities have different public keys (because their incantations differ), and IAM treats them as completely separate. The canonical identity is always the public key, never the name. When two members of the same graph share a name, runtime layers MUST disambiguate using public key suffixes (the same mechanism used for seal collisions).
A consequence of including name in the KDF salt is that two identities sharing a name slug also share the same Argon2id salt input. This slightly increases the efficiency of brute-force attacks against common names: an attacker who builds a precomputation for a popular name can reuse it against every identity sharing that slug. Users SHOULD avoid trivially common names (such as “alice”, “bob”, “admin”, “root”, “test”) for this reason. Less common names provide better isolation against shared-precomputation attacks without otherwise affecting identity uniqueness.
An incantation is a user-chosen pass phrase. It MUST match the regular expression ^[\x20-\x7E]{12,}$.
Printable ASCII only (0x20 through 0x7E), minimum 12 characters, no upper limit. This excludes all Unicode, all control characters, all zero-width characters, and all invisible whitespace variants that could silently differ between entry and recovery.
Implementations MUST NOT perform Unicode normalization, case folding, whitespace trimming, quote substitution, or any other transformation on the incantation bytes.
Implementations SHOULD provide a means for the user to see the exact bytes they are entering during creation, to avoid silent differences caused by autocorrect, input method editors, or keyboard layout quirks.
The minimum length of 12 filters out trivially short inputs. Real strength depends on unpredictability of the chosen phrase, not on length alone. Users are strongly encouraged to use full sentences with multiple words.
This specification uses the following exact encodings:
public key 32 bytes, encoded as 64 lowercase hexadecimal characters
([0-9a-f]), no prefix, no separators.
record id 32 bytes (SHA-256 output), encoded as 64 lowercase
hexadecimal characters.
signature 64 bytes (Ed25519 output), encoded as standard base64
per RFC 4648 section 4, with '=' padding, 88 characters total.
Implementations MUST use these exact forms. Uppercase hex, url-safe base64, or base64 without padding are all invalid.
Given a valid name and an incantation:
name_bytes = UTF-8 bytes of name (ASCII)
inc_bytes = bytes of incantation (ASCII, exact)
salt = SHA-256("iam:v1:name:" || name_bytes)[0:16]
// 16 bytes = 128 bits, conforming to RFC 9106 salt recommendation
master_seed = Argon2id(
password = inc_bytes,
salt = salt,
memory = 65536 KiB (64 MiB),
iterations = 3,
parallelism = 1,
tag_length = 32
)
root_seed = HKDF-SHA256(
ikm = master_seed,
salt = empty,
info = "iam/v1/root-ed25519",
length = 32
)
root_key = Ed25519.keypair_from_seed(root_seed)
The derivation is deterministic. The same name and incantation MUST always yield the same root key on any compliant implementation.
Argon2id parameters are fixed for iam-core 1.0. Changing them would create a different identity space and MUST be done only through a new IAM version with a new salt prefix (for example iam:v2:name:).
Note on parallelism: RFC 9106 recommends p=4 for the 64 MiB / 3-iteration profile. IAM uses p=1 as a deliberate trade-off to ensure derivation works on weak devices (phones, Raspberry Pi, older laptops) where allocating four parallel 64 MiB buffers is impractical. The reduction in parallelism slightly weakens resistance to offline brute force compared to the RFC 9106 default.
The security of an IAM identity is bounded by the unpredictability of the incantation. Argon2id provides resistance to offline brute force, but cannot make a weak phrase strong. An attacker who knows or guesses a target’s name (which is public) can attempt offline derivation against the published public key without rate limiting, without network interaction, and without observable activity.
This is a structural property of the design, not a bug. IAM identities are meant to be reproducible from human memory on any device without server interaction. Deterministic derivation is the price of that property.
Implementations and users MUST treat the incantation as a high-value secret:
name is public by design, choosing names that match common username patterns (real-name dot last-name, etc.) makes targeted attacks easier.Additional keys for specific purposes are derived from master_seed via HKDF-SHA256. The root Ed25519 key is one such derivation:
purpose_seed = HKDF-SHA256(
ikm = master_seed,
salt = empty,
info = "iam/v1/" || purpose_name,
length = 32
)
Implementations MUST use "iam/v1/root-ed25519" as the info string for the root signing key. Other purposes (encryption keys, device delegation, session keys) MAY use other info strings under the same scheme.
The seal is derived from the public key using the canonical wordlist:
pub_hash = SHA-512(public_key)
seal[i] = WORDLIST[pub_hash[i]] for i in 0..3
The wordlist is fixed by this specification (see appendix). It contains exactly 256 words, indexed from 0 to 255. Implementations MUST use this exact wordlist. Changing a single word creates an incompatible seal space.
Four words from 256 give 32 bits of fingerprint. This is sufficient for visual recognition, not sufficient for global uniqueness.
An identity exists within two kinds of trust structure that share the same rules:
personal graph devices owned by one identity
community graph identities accepted by other identities
Both use the same record types, the same validation, and the same signatures. They are distinguished by the context field on every record.
An identity MAY participate in multiple community graphs simultaneously. Each graph is independent. Each community graph is identified by its tree id, which is a hash of its bootstrap list (see Genesis section). A personal graph is identified by the owner’s root public key.
alice (root_key, implicit root)
|
+-- laptop
+-- phone
+-- jan-home
+-- jab-browser
Each device has its own Ed25519 keypair generated locally. The device private key MUST NOT be transmitted. The device public key is transmitted via the ACCEPT record that places the device in the personal graph.
The root key signs structural operations (ACCEPT and REVOKE of devices). Device keys sign session traffic and ordinary messages defined by JAM runtime. This separation means the incantation must be entered only when the graph structure changes, not during normal use.
Implicit root: For context = "personal", the public key equal to tree is the implicit root of that personal graph. It is valid by definition and requires no ACCEPT record. All other members of the personal graph MUST have a valid lineage to this implicit root.
No LEAVE in personal: LEAVE records in context = "personal" MUST be rejected. An identity cannot leave its own personal graph. Devices are removed via REVOKE signed by the root; the root itself exists as long as the identity exists.
tree: <tree_id from bootstrap list>
bootstrap root members: alice, bob (axiomatically valid)
alice
+-- charlie
+-- dave
bob
+-- eve
+-- charlie (also accepted by bob, multiple parents)
Bootstrap root members are axiomatically valid: they are named in the bootstrap list, and that naming is their claim to membership. They have no parent in the graph. Members below them are accepted by existing valid members via ordinary ACCEPT records. A member MAY have multiple parents (as charlie above, accepted by both alice and bob).
Every community graph is identified by the SHA-256 hash of its bootstrap list, not by the genesis public key. See the Genesis section for the bootstrap construction and why the tree id is content-bound to its bootstrap.
All structural records (ACCEPT, REVOKE, LEAVE) MUST be signed by the actor’s root key.
Device keys MUST NOT sign structural records. Device keys sign only session authentication and ordinary messages as defined by JAM runtime.
This rule is strict in iam-core 1.0. There is no delegation from root to device for structural operations. When a user adds or removes a device, or performs a graph operation, the incantation must be present to recompute the root key. Structural changes are deliberate and rare.
A device signature carries authority only if the verifier possesses a valid, unrevoked personal ACCEPT chain from that device key to the claimed root key.
How personal graph records are distributed to verifiers is a runtime concern and is not specified by IAM.
Note on stale device acceptance: a device REVOKE issued by an owner takes effect for any verifier only after the verifier has received and admitted the REVOKE record. A revoked device may still appear valid to verifiers who have not yet received the REVOKE. This is the standard limitation of any distributed revocation system. Runtime layers SHOULD use mechanisms such as short-lived signatures, active replication policies, and periodic re-verification to bound the window of stale acceptance. IAM does not specify these mechanisms.
If a root key is compromised, recovery within iam-core 1.0 is not possible at the protocol level. The compromised identity is structurally tied to its derivation from name and incantation; there is no mechanism to rotate the underlying root key while preserving membership.
This is a deliberate constraint of the constitution. Key rotation introduces complex semantics about which signatures remain valid across rotation events, which records are bound to which key, and how transitively trusting parties learn about the rotation. iam-core 1.0 does not attempt these.
Practical recovery requires social action:
name, a new incantation, or both). The new identity has a new root key, a new seal, and no prior history.This is intentionally heavy. Compromise of a root key SHOULD be a rare event, treated with the same gravity as the loss of a long-held cryptographic key in any other system. Users who anticipate frequent rotation needs are using IAM for a use case it was not designed for.
Future IAM versions MAY add key rotation primitives. Runtime layers MAY implement rotation through composition of existing primitives plus runtime metadata, without modifying the constitution.
Compromise of a bootstrap root member key is the most severe failure mode in a community tree. Because bootstrap root members cannot be REVOKEd by any other member, and cannot be structurally removed from the axiomatic set except through their own LEAVE, a compromised bootstrap root member is effectively uncontainable within the tree.
If the attacker holds the key, they can refuse to LEAVE and continue signing ACCEPTs indefinitely. The community only recourse is to abandon the compromised tree and create a new tree with a new bootstrap list that excludes the compromised member. The old tree continues to exist as a historical artifact but receives no further social recognition.
This is a strong argument for using multiple bootstrap root members rather than a single point of compromise, and for treating bootstrap root member keys with the same operational gravity as genesis itself.
Three record types define all changes to a graph:
ACCEPT actor accepts target as a new child in (tree, context).
target may already have other parents; ACCEPT adds actor as
an additional parent.
REVOKE actor removes the parent-child edge from actor to target.
target's lineage through actor is broken; other lineages
through other parents (if any) remain intact.
LEAVE actor removes itself from (tree, context).
all edges incident to actor (both incoming and outgoing)
become inactive. if actor was an axiomatic member, that
status is removed. departure is permanent within the
evaluation. not valid for context = personal.
All three are unilateral: a single actor signature suffices. There are no handshakes, no bilateral records, no co-signatures in iam-core 1.0.
A record is a JSON object with the following fields, serialized using JSON Canonicalization Scheme (RFC 8785). The input to JCS MUST be valid I-JSON (RFC 7493). Implementations MUST reject any record whose JSON representation contains duplicate member names within an object.
{
"v": 1,
"kind": "ACCEPT" | "REVOKE" | "LEAVE",
"tree": <64 lowercase hex chars>,
"context": "personal" | "community",
"m": <integer>,
"actor": <64 lowercase hex chars>,
"target": <64 lowercase hex chars>,
"prev": <64 lowercase hex chars or empty string>,
"body": {}
}
Field meanings:
v is the IAM version. For iam-core 1.0 this MUST be 1.kind is the record type.tree is the identifier of the graph this record belongs to. It is 64 lowercase hex characters representing a SHA-256 hash, computed as follows:
for context = "community", tree MUST equal the tree id of a known community graph. The tree id is defined in the Genesis section as:
tree_id = lowercase_hex(
SHA-256("IAM1:tree\0" || JCS([desc_1, desc_2, ..., desc_n]))
)
where each desc_i is the reduced descriptor form of the i-th bootstrap ACCEPT record (containing only v, kind, context, m, actor, target — no tree, prev, body, id, or sig). This exact formula is the sole definition of tree id; no other description in this specification is authoritative.
for context = "personal", tree MUST equal the root public key of the identity (which also equals actor for records made by the owner). Personal graphs have no bootstrap list; the implicit root is the anchor.
Note: for community context, tree is a hash, not a public key. The genesis public key is recoverable from the bootstrap records but is not itself the tree identifier. This makes the tree identifier mathematically bound to its exact bootstrap content: no alternative bootstrap can claim the same tree id.
m is the timestamp (see Time section).actor is the root public key of the signer.target is the affected member. For LEAVE, target MUST equal actor.prev is the id of the previous record by the same actor in the same (tree, context), or the empty string for the first record.body MUST be an empty object {} in iam-core 1.0. This field is reserved for future IAM versions. Runtime metadata MUST NOT be placed here; it belongs in the runtime wrapper outside the IAM record.id = lowercase_hex(SHA-256("IAM1:id\0" || JCS(record)))
where JCS is the canonical JSON serialization of the record object (before any signature is added).
sig = Ed25519.sign(
key = actor_root_key,
msg = "IAM1:record\0" || JCS(record)
)
The signature is encoded as 88-character standard base64 (RFC 4648 section 4, with padding).
The complete stored form of a record is:
{
"record": <canonical record>,
"id": <64 lowercase hex chars>,
"sig": <88-char base64 signature>
}
Implementations MUST verify the record against its id and signature before accepting it into any evaluation.
IAM evaluation requires two distinct inputs, which together describe everything an implementation needs to compute a graph state:
known_trees a registry of verified bootstrap lists, one per tree id
records a set of signed IAM records
Both inputs pass through a pipeline of four layers: observation, admission, anchoring, and evaluation.
known_trees is a map from tree id to an ordered bootstrap list:
known_trees: { tree_id -> ordered_bootstrap_list }
A tree id is in known_trees only if:
For context = "personal", there is no bootstrap and therefore no entry in known_trees. Personal graphs are implicitly known by their owner’s root public key, which serves as the tree field.
known_trees is the set of community graphs this implementation recognizes. A record with context = "community" and tree = X can be admitted only if X is in known_trees.
In addition to known_trees, records themselves pass through three layers. Implementations MUST NOT conflate them.
The set of records an implementation has seen via any transport. This is a runtime concern; IAM does not specify how records are obtained, distributed, or discovered. Observed records may include duplicates, malformed entries, records from unrelated trees, and entries received in arbitrary order.
Admission is the local policy by which an implementation moves a record from observation into its local store. Admission uses the implementation’s wall clock and is necessarily a local decision.
Admission has two tracks: one for bootstrap lists and one for ordinary records.
Bootstrap admission. A bootstrap list is admitted into known_trees if:
tree field carried by all records in the list.actor (genesis public key).A verified bootstrap list is added to known_trees under its tree id. A bootstrap list that fails any check is rejected.
Record admission. An ordinary (non-bootstrap) record is admitted if:
context = "community", tree is already in known_trees. A record referencing an unknown tree MAY be held in quarantine pending bootstrap admission for that tree.m <= now + 5 where now is the implementation’s current clock in minute units. Records with m further in the future are held in quarantine and MAY be retried as time advances.prev either equals the empty string or equals the id of a record already admitted for the same (tree, context, actor). Records with unknown prev MAY be held in quarantine pending arrival of the predecessor.Admission is local. Two implementations may admit the same record at different times. Admission is NOT canonical evaluation. Two implementations with identical admitted sets but different anchored sets MAY compute different graph states; that is correct and expected.
The anchored set is the subset of admitted records over which canonical evaluation runs. The anchored set is chosen by the implementation, the operator, or the community using runtime-defined policy. IAM does not specify how anchoring decisions are made.
An anchored set MUST be closed under prev: for every record r in the set, if r.prev is non-empty, then the record with id equal to r.prev MUST also be in the set.
An anchored set is evaluated together with the current known_trees registry. For any record r in the anchored set with context = "community" and tree = X, the tree id X MUST be present in known_trees. An anchored set that references an unknown tree is incomplete and MUST cause evaluation to abort with an error; the implementation should first admit the missing bootstrap list before attempting evaluation. Returning an empty state is NOT a valid response to an unknown tree.
This rule makes bootstrap loss impossible within any live evaluation: evaluation cannot proceed without the bootstrap being in known_trees, and known_trees is populated only by verified bootstrap lists. Bootstrap replication is a prerequisite of holding graph state, not a separate concern.
Anchoring separates transport from truth. A community recovering from a fork can choose an anchored set that excludes one branch. A new joiner can adopt an anchored set provided by a trusted peer (typically including both the bootstrap list for known_trees and a set of records). A historical audit can replay a graph at a past anchor. None of these change what was observed or admitted; they change what is considered authoritative for evaluation.
Canonical evaluation is a pure function of its two inputs: the known_trees registry and an anchored record set. It MUST NOT depend on wall-clock time, arrival order, network topology, or any local state beyond these inputs.
evaluate(known_trees, anchored_set, tree, context) -> graph state
Given the same known_trees and the same anchored record set, any two implementations MUST compute identical graph state. This is the determinism guarantee of IAM.
For the evaluation to be well-defined, known_trees[tree] MUST exist when context = "community". If context = "personal", known_trees is not consulted; the implicit root is derived directly from the tree field.
A record is structurally valid if:
v equals 1.kind is one of ACCEPT, REVOKE, LEAVE.context is one of personal, community.tree is a valid 64-character lowercase hex string.context = personal, tree equals actor (the implicit root is the owner).context = community, tree is present in known_trees (the tree has been admitted, its bootstrap list verified).m is a non-negative integer.actor and target are valid 64-character lowercase hex public keys.actor != target.target equals actor.context = personal, the record is REJECTED.prev is either the empty string or a 64-character lowercase hex id.prev is non-empty, the record referenced by prev (when available during evaluation) MUST have the same (tree, context, actor) as this record. Evaluation rejects records whose prev points outside their own chain.body equals {}.id matches SHA-256 of the canonical serialization.sig verifies against actor over the domain-separated canonical bytes.A record failing any of these checks is rejected and does not participate in evaluation.
Within the same (tree, context, actor), m MUST be non-decreasing along the prev chain:
record.m >= prev_record.m
A record whose m is strictly less than the m of its prev is rejected. Equal m values are permitted because ordering within a chain is already determined by prev.
If two distinct records from the same (tree, context, actor) share the same prev value, the actor’s chain in that (tree, context) has forked. Both records remain individually signed, but the chain is considered broken from the fork point forward.
Implementations MUST NOT pick a winner based on m, id, or any other field. Both records exist as evidence of misbehavior. No further records from the same actor in the same (tree, context) that depend on either fork branch are valid.
Recovery from a fork is handled at the anchoring layer, not within canonical evaluation. The community or operator chooses an anchored set that excludes the fork (typically by omitting one of the conflicting records and everything causally downstream of it). Within any anchored set that contains both fork records, the affected actor’s chain is broken from the fork point and remains broken; canonical evaluation has no notion of repair.
A target MAY be accepted by multiple actors in the same (tree, context). Each ACCEPT record creates an independent parent-child edge. The target has as many parents as there are active (non-revoked) ACCEPT records pointing to it.
Each parent-child edge is independent. REVOKE by one parent breaks only that edge. The target remains a member of the graph as long as at least one parent edge remains and a valid lineage exists from that parent back to an axiomatic member (a bootstrap root member for community, or the implicit root for personal).
This makes the structure a directed acyclic graph, not strictly a tree. Cycles are prevented by the rule below.
An ACCEPT record is rejected if it would create a cycle: if actor is itself a descendant of target in the current (tree, context) state, the ACCEPT is invalid.
This check is performed against the evaluated state before the ACCEPT is applied. If the ACCEPT would make target an ancestor of actor (directly or transitively), it is rejected.
Beyond structural validity, an ACCEPT is semantically valid if:
actor != target. A member cannot ACCEPT themselves.actor is a currently valid member of (tree, context) at evaluation time.actor is not in state.departed. A member who has LEFT cannot issue ACCEPTs in the same evaluation.target is not in state.departed. A member who has LEFT cannot be re-accepted within the same evaluation. Departure is permanent within a single evaluation.actor to the same target in the same (tree, context) already exists. Re-ACCEPTing after REVOKE is permitted; duplicate active ACCEPT is not.A revoked or departed actor MUST NOT issue valid ACCEPTs. An actor whose membership was lost (because they were revoked, they LEFT, or the chain providing their membership was broken) cannot vouch for new members until they themselves are re-accepted.
Bootstrap records are a special case. Bootstrap records are not ordinary ACCEPT records subject to this validity rule. They are processed separately in Phase 0 of canonical evaluation and validated against Bootstrap constraints (see Genesis section). The genesis public key is not a member of the graph; it is the ephemeral signer of the bootstrap, nothing more. Bootstrap records do not create parent-child edges in the graph — they directly establish their targets as axiomatic members (bootstrap root members) of the tree.
Personal context. For context = personal, the implicit root is axiomatically valid. The implicit root is the public key whose value equals the tree field (which in personal context is itself a public key, the owner’s root pubkey). The implicit root may ACCEPT any number of devices. All non-root members of a personal graph must have a valid lineage back to the implicit root.
Note on phantom acceptance: ACCEPT is unilateral. The target does not consent to being accepted and may not be aware of it. A valid actor may ACCEPT any public key, including keys whose owners are unrelated to the graph, do not exist, or did not authorize the action. Such phantom acceptances are structurally valid but practically inert: a phantom member cannot sign records (no one holds the private key), cannot vote, cannot participate, and cannot REVOKE or LEAVE. Phantom members appear in the graph topology but contribute nothing. Runtime layers MAY filter or hide phantom members based on activity criteria; iam-core does not.
A REVOKE is semantically valid if:
actor is a currently valid member of (tree, context) at evaluation time. A revoked or departed actor MUST NOT issue valid REVOKEs over their past edges.actor to target exists in (tree, context) at evaluation time.actor != target.REVOKE breaks only the specific parent-child edge from actor to target. Other edges to target (from other parents, if any) remain. If target has no remaining active parent edges and is not an axiomatic member of the graph, target loses membership.
Descendants of target that depended exclusively on the lineage through this revoked edge also lose their valid lineage. Descendants with alternative lineages through other parents remain valid.
Bootstrap root members cannot be revoked within their own tree. Because bootstrap records do not create parent-child edges (they establish axiomatic membership instead), there is no ACCEPT edge pointing at a bootstrap root member within the tree where they are axiomatic. Therefore no actor within that tree can satisfy REVOKE validity condition 2 against a bootstrap root member. This is a structural property of Model B, not an oversight.
A bootstrap root member can exit the graph in exactly two ways:
state.axiomatic. See LEAVE validity below.Compromise of a bootstrap root member’s key is therefore a severe event: the compromised member cannot be REVOKEd by any other member. The only remedies are self-LEAVE (which requires the compromised key to cooperate), or creation of a new tree. Implementations and operators SHOULD treat bootstrap root member key compromise with maximum gravity.
A LEAVE is semantically valid if:
context = community (LEAVE in personal is rejected).actor is a currently valid member of (tree, context) at evaluation time, either through an active ACCEPT lineage or through axiomatic status.target = actor.LEAVE removes the actor from the active graph. Three simultaneous effects occur:
actor (both incoming and outgoing) become inactive in state.edges.actor was an axiomatic member, actor is removed from state.axiomatic. A former axiomatic member has no surviving claim to membership unless some other currently valid member accepts them via an ordinary ACCEPT.actor is added to state.departed, marking the departure as permanent within the current evaluation.After a valid LEAVE, the actor is no longer a member by any path.
Children of actor lose any lineage that passed through actor; they retain any other lineages they may have through other parents. Members who had accepted actor (parents of actor) lose that specific edge to actor; their own position in the graph is otherwise unchanged.
LEAVE is the only way a bootstrap root member can exit their own tree. Because bootstrap root members have no parent edge, no other member can REVOKE them. LEAVE by the bootstrap root member themselves is the only protocol-level mechanism for their removal from the axiomatic set.
Canonical evaluation is a pure function:
evaluate(known_trees, anchored_set, tree, context) -> graph state
The anchored set is provided externally (see Anchoring section). It MUST be closed under prev: for every record r in the anchored set, if r.prev is non-empty, then the record with id equal to r.prev MUST also be in the set.
Evaluation is a topological traversal over prev chains, with (m, id) ordering as a deterministic tiebreaker among records that are simultaneously eligible. The algorithm makes every check explicit.
Before the main loop, two preflight checks run on the anchored set:
// Preflight 1: anchored set must not contain bootstrap records
for each r in anchored_set:
if r.tree in known_trees AND r is in known_trees[r.tree]:
ABORT: anchored set contains a bootstrap record
// Preflight 2: anchored set is filtered to the requested graph
scope := { r in anchored_set :
r.tree == tree AND r.context == context }
All subsequent work operates on scope, not on the full anchored set. Bootstrap records are NOT in scope, because they MUST NOT be in anchored_set in the first place. They are read exclusively from known_trees[tree] in Phase 0.
Bootstrap records MUST NOT appear in anchored_set. They live exclusively in known_trees. An implementation that finds a bootstrap record in an anchored set MUST abort evaluation and treat the anchored set as malformed. The two data structures are disjoint by construction: known_trees holds bootstrap lists indexed by tree id, anchored_set holds ordinary records that reference those tree ids via their tree field.
The graph state is a structure with at least:
state.edges active ACCEPT edges (parent, child)
state.axiomatic set of currently active axiomatic members
state.departed set of members who have LEFT (for LEAVE of target check)
The algorithm is:
state := empty graph state
processed := empty set
broken_chains := empty set // (tree, context, actor) tuples
// Phase 0: bootstrap (community context only)
if context == "community":
if tree not in known_trees:
ABORT: anchored set references an unknown tree
bootstrap := known_trees[tree] // ordered bootstrap list
for each r in bootstrap (in bootstrap list order):
state.axiomatic := state.axiomatic + { r.target }
// Note: bootstrap records are NOT in the anchored set's scope
// and are NOT subject to main-loop processing. They
// establish axiomatic membership directly. The genesis
// public key is never a node in the graph.
// For personal context, the implicit root is axiomatic
if context == "personal":
state.axiomatic := state.axiomatic + { tree } // tree == root pubkey
// Phase 1: topological traversal of non-bootstrap records
non_bootstrap := scope - processed
ready := { r in non_bootstrap : r.prev = "" OR
anchored_set[r.prev] in processed }
while ready is not empty:
r := record in ready with smallest (m, id)
ready := ready - { r }
chain_key := (r.tree, r.context, r.actor)
// Always enqueue children BEFORE any rejection path,
// so the traversal reaches every reachable record.
for each c in non_bootstrap with c.prev == r.id:
if c not in processed and c not in ready:
ready := ready + { c }
// Step 1: structural validity
if not structurally_valid(r):
processed := processed + { r }
continue
// Step 2: prev chain integrity (for non-bootstrap records)
if r.prev != "":
if r.prev not in anchored_set:
processed := processed + { r }
continue
prev_record := anchored_set[r.prev]
if prev_record.tree != r.tree OR
prev_record.context != r.context OR
prev_record.actor != r.actor:
processed := processed + { r }
continue
// Step 3: monotonicity along prev chain
if r.m < prev_record.m:
processed := processed + { r }
continue
// Step 4: chain not already broken by an earlier fork
if chain_key in broken_chains:
processed := processed + { r }
continue
// Step 5: fork detection
// (any other record in non_bootstrap with same chain_key
// and same prev value as r constitutes a fork)
siblings := { s in non_bootstrap :
s != r AND
(s.tree, s.context, s.actor) == chain_key AND
s.prev == r.prev }
if siblings is not empty:
broken_chains := broken_chains + { chain_key }
processed := processed + { r }
continue
// Step 6: semantic validity against current state
if not semantically_valid(r, state):
processed := processed + { r }
continue
// Step 7: apply
if r.kind == ACCEPT:
state.edges := state.edges + active_edge(r.actor, r.target)
elif r.kind == REVOKE:
mark edge (r.actor, r.target) inactive in state.edges
elif r.kind == LEAVE:
// Break all edges where r.actor is either endpoint
mark all edges incident to r.actor inactive in state.edges
// If r.actor is an axiomatic member, remove that status
state.axiomatic := state.axiomatic - { r.actor }
// Record the departure so that LEAVE-of-target checks
// can invalidate edges where the former target has left
state.departed := state.departed + { r.actor }
processed := processed + { r }
return state
Notes on the algorithm:
ready BEFORE any rejection check. This ensures that the traversal reaches every reachable record in the prev chain, so that fork detection can discover fork siblings, and so that downstream errors are observed, not hidden.Two implementations running this algorithm on the same anchored set MUST produce identical state for the same (tree, context).
Why prev rather than (m, id) alone: a single actor in a single (tree, context) can produce multiple records in the same minute. The prev chain determines causal order within an actor. If two records share the same prev, that is a fork (see Fork detection) and the chain is broken from that point.
Why (m, id) as tiebreaker: when records from different actors become eligible simultaneously (their respective prev records have all been processed), some deterministic order is needed. (m, id) ascending provides it. This ordering does NOT pick winners in conflicts; conflicts are handled explicitly by fork detection and multi-parent acceptance.
A member is considered present in the graph state if any of the following holds:
state.axiomatic (a currently active axiomatic member: a bootstrap root member in community context, or the implicit root in personal context, who has not LEFT).Axiomatic members are the base case of membership. A bootstrap root member is initially in state.axiomatic after Phase 0. If they LEAVE, they are removed from state.axiomatic and added to state.departed. Within the same evaluation, departure is permanent: a departed member cannot be re-accepted (see ACCEPT validity rule 4). A different anchored set (one that excludes the LEAVE record, by social or operational decision at the anchoring layer) would yield a different state in which the member is still axiomatic. This is the only way “return” is meaningful, and it is a property of anchoring, not of evaluation.
All other (non-axiomatic) members are derived by ACCEPT chains leading back to at least one currently active axiomatic member. Neither tree nor the genesis public key are themselves nodes in the graph; they are identifiers. Genesis signs the bootstrap list and is then destroyed; it never exists as a member.
These are two distinct concepts. Implementations MUST NOT conflate them.
A lineage is a chain of active ACCEPT records from a member back to an axiomatic member. Because a member may have multiple parents, a member may have multiple lineages. Each lineage proves one path of origin.
charlie <- accepted by alice (alice is an axiomatic bootstrap root member)
charlie <- accepted by bob (bob is also an axiomatic bootstrap root member)
(charlie has two lineages)
Lineage verification walks each ACCEPT record in turn and checks its signature, then confirms that the lineage terminates at an axiomatic member of the tree. Verification of one lineage is O(depth) Ed25519 operations plus one bootstrap membership lookup.
Membership is the evaluated state of the graph at a specific (known_trees, anchored_set) pair. A member is currently present in the graph if:
An active ACCEPT is one that has not been broken by any of the following:
When either endpoint of an edge leaves the graph, the edge becomes inactive.
Lineage proves that a path of origin existed at some point. Membership proves that at least one such path is currently intact.
m = floor(unix_seconds / 60) - 28928160
One tick is one minute. Epoch is 2025-01-01 00:00 UTC.
Every record carries m. m provides chain monotonicity, freshness bounds during admission, and deterministic evaluation order. Implementations MUST use integer minute granularity.
Note: m is set by the signer and cannot be trusted as an objective time source for conflict resolution. IAM uses m only for chain ordering (where prev already establishes causality) and freshness checks during admission. m is NOT used to pick winners in forks or multi-parent situations.
Prev chains establish causality within a single actor’s sequence of records, but IAM has no direct mechanism to express “record R depends on an earlier record S from a different actor”. If the validity of a record depends on the prior acceptance of the actor by another member, that ordering is enforced only through the (m, id) deterministic tiebreaker among simultaneously eligible records.
For example: Alice ACCEPTs Bob, then Bob ACCEPTs Charlie. If Alice’s and Bob’s records both have the same m, the evaluation algorithm processes them in (m, id) order. If Bob’s record is processed before Alice’s (because Bob’s id happens to sort earlier), Bob is not yet a valid member when his ACCEPT is evaluated, and his record is rejected.
This is deterministic (every implementation reaches the same result) but it is a sharp edge. To avoid it, records whose validity depends on an earlier record by another actor SHOULD have m strictly greater than the m of that dependency. Runtime layers MAY enforce this by delaying publication of dependent records, or by rejecting attempts to submit records with insufficient m.
IAM does not attempt to enforce this constraint structurally, because doing so would require tracking inter-actor causal links, which is not part of the record format. Implementations and users are expected to respect monotonic wall-clock time in practice.
A community graph begins with genesis. Genesis produces the bootstrap list: the ordered sequence of initial ACCEPT records that seed the graph’s first root members. The bootstrap list is the eternal anchor of the graph. Order is part of the identity of the graph: two bootstrap lists with the same records in different order produce different tree ids and are different graphs.
The term “bootstrap list” is used throughout this specification in preference to “set”, because ordering is significant. Implementations MUST preserve the order of bootstrap records as published.
Three related but distinct concepts appear in the community graph and MUST NOT be confused:
actor field. It is recoverable from any bootstrap record but is not itself a member of the graph. The corresponding private key is destroyed immediately after bootstrap signing.Personal graphs have a simpler structure: only tree and an implicit root, both equal to the owner’s Ed25519 root public key. The implicit root IS a member of the personal graph.
A community graph is bootstrapped by an atomic batch operation: genesis signs an ordered set of ACCEPT records that seed the initial root members. All bootstrap records share the same m, all have prev = "", and their order is part of the tree identity.
Bootstrap records do NOT form an internal prev chain. They are parallel, not sequential. Their order is determined by their position in the bootstrap list, which is itself the input to the tree id hash. This avoids the circular dependency that would otherwise arise (record id depends on tree, tree depends on bootstrap content, bootstrap content includes record ids).
To create a new community graph:
1. Generate 32 random bytes as a seed. These bytes MUST come from a
cryptographically secure random source. They MUST NOT be derived
from any incantation or other recoverable value.
2. root_seed = HKDF-SHA256(ikm=seed, salt=empty,
info="iam/v1/root-ed25519", length=32)
3. genesis_key = Ed25519.keypair_from_seed(root_seed)
4. Decide the ordered list of initial root members
[target_1, target_2, ..., target_n]. Order is part of the
bootstrap and is preserved through the tree id hash.
5. Choose a single bootstrap timestamp m_b (current m value).
6. For each target_i in order, construct a bootstrap descriptor
(a reduced record form, used only for tree id computation):
desc_i = {
"v": 1,
"kind": "ACCEPT",
"context": "community",
"m": m_b,
"actor": genesis_key.pub,
"target": target_i
}
Note: desc_i has neither tree, prev, body, id, nor sig.
7. Compute the tree id from the ordered descriptor list:
tree_id = lowercase_hex(
SHA-256("IAM1:tree\0" || JCS([desc_1, desc_2, ..., desc_n]))
)
8. For each i, construct the full bootstrap record by adding
tree, prev, and body to desc_i:
record_i = desc_i + {
"tree": tree_id,
"prev": "",
"body": {}
}
9. Compute id and sig for each full bootstrap record:
id_i = lowercase_hex(SHA-256("IAM1:id\0" || JCS(record_i)))
sig_i = Ed25519.sign(genesis_key,
"IAM1:record\0" || JCS(record_i))
10. The seed and private key MUST be erased from memory immediately
after all bootstrap records have been signed.
11. The bootstrap list is published as the graph's permanent
anchor. The published form includes tree_id, ids, signatures,
and the original ordering of records.
The tree id is computed from a reduced descriptor form, not from the full records. This is the only place in IAM where a hash is computed over something other than a complete canonical record. The reduction is necessary and sufficient: it captures everything that defines the bootstrap (genesis pubkey via actor, root members via target, batch time via m, and order via JCS array order) while excluding fields that would create circular dependencies (tree, prev, id, sig).
Every bootstrap record MUST satisfy all Structural validity rules (see Canonical evaluation section), with one exception:
tree to equal the tree id of a KNOWN community graph) is relaxed for bootstrap records, because bootstrap records define the tree id rather than reference an existing one. A bootstrap record’s tree field MUST equal the tree id computed from the bootstrap list itself (per the Bootstrap creation formula).All other structural validity rules apply unchanged: v = 1, valid hex lengths, actor != target, valid signature over the domain-separated canonical form, and so on.
In addition to structural validity, a valid bootstrap list MUST satisfy the following bootstrap-specific constraints:
kind = "ACCEPT" and context = "community".actor value (the genesis public key).m value (the bootstrap timestamp).prev = "" and body = {}.tree field, equal to the computed tree id.target values are pairwise distinct. No member appears twice in the bootstrap list.actor (genesis public key).actor value does not appear as any target (genesis does not accept itself).A bootstrap list that fails any structural validity rule or any bootstrap-specific constraint is invalid and produces no valid tree. Implementations MUST verify all constraints before accepting a tree as known.
The relaxation of structural validity rule 6 is the only privilege bootstrap records receive. In every other respect, they are ordinary records that happen to be signed by an ephemeral key.
Genesis has no human. No incantation ever existed. The private key lives in one machine’s memory only long enough to sign the bootstrap records. After erasure, genesis cannot sign anything else. It is dead by construction.
Implementations SHOULD expose this process as a single command (for example iam genesis). The erasure in step 10 is MUST.
The tree identifier for a community graph is a SHA-256 hash of the bootstrap content, not a public key. This matters:
Bootstrap records satisfy ACCEPT validity as a special case: they are the records that define the tree. The condition “actor is a currently valid member” is replaced by “the record is one of the bootstrap records for this tree”. This breaks the otherwise circular requirement that genesis itself be a member before accepting anyone.
Bootstrap records share the same (tree, context, actor, prev) tuple by design: they all have context = "community", the same tree (the tree id they define), the same actor (genesis), and prev = "". Under the general Fork detection rule, this would classify the bootstrap as a forked chain and invalidate every bootstrap record.
Bootstrap is an explicit exception to fork detection. The rule is:
Fork detection does NOT apply to records belonging to a bootstrap
list. A record is a bootstrap record for tree X if and only if
it is part of the bootstrap list whose computed tree id equals X.
Bootstrap records are instead validated as an atomic batch: either the entire bootstrap list is valid (all constraints met, tree id matches, all signatures valid) or the entire list is rejected. There is no partial bootstrap.
After the bootstrap batch is validated, subsequent records from any actor (including genesis, if it were somehow to sign more records — which it cannot, because its private key has been erased) follow the normal Fork detection rules. But since genesis cannot sign anything after erasure, its chain effectively ends with the bootstrap batch.
A record is a bootstrap record for tree X if and only if it appears in the bootstrap list whose computed tree id equals X. To verify a claimed bootstrap list:
1. Strip each record to its descriptor form (v, kind, context, m,
actor, target only).
2. Verify that all records share the same m (the bootstrap timestamp).
3. Verify that all records have prev = "" and body = {}.
4. Compute SHA-256("IAM1:tree\0" || JCS([desc_1, ..., desc_n])).
5. Compare the result with the claimed tree id.
6. Verify each full record's id and sig independently.
If all checks pass, the bootstrap list is authentic for the given tree id.
Once published, the bootstrap list is immutable. Its tree id is computed from its content; any change produces a different tree id and therefore a different tree. All bootstrap records have prev = ""; they do not form a sequential chain among themselves.
Records that come after bootstrap follow the ordinary prev chain rules. The prev field is defined per-actor per-(tree, context), so the first record by any root member in a given (tree, context) has prev = "", regardless of whether bootstrap records exist for that tree. Bootstrap records belong to genesis’s chain, not to the root members’ chains; a root member’s own chain is independent and begins fresh at their first structural action.
If all copies of the bootstrap records for a given tree id are lost, the tree is dead. No party can sign new records that would be valid, because no member can prove their lineage to a root that cannot be verified. Runtime layers are expected to replicate bootstrap records aggressively and persistently, but IAM does not mandate any particular replication scheme.
A tree is a living system. Living systems can die. Death of a tree is not a protocol failure; it is a natural outcome when its community fails to preserve its foundation. The remedy is to create a new tree (new genesis, new bootstrap, new tree id) and re-establish the community within it. The old tree remains as a historical fact; it simply has no evaluable state.
Personal graphs use tree = actor_root_pubkey. There is no bootstrap list and no genesis process for personal graphs. The implicit root is valid by definition as stated in the Fractal structure section.
IAM does not define how records are stored, how they are transported between nodes, or how nodes discover each other. These are JAM runtime concerns.
Common choices for storage include git repositories, append-only log files, embedded databases, and distributed hash tables. Common choices for transport include WebSocket, HTTP, and peer-to-peer overlays. IAM is agnostic to all of them.
What IAM requires of any host:
- Records are identified by their canonical id (SHA-256 of canonical JSON).
- Admission uses local clock for freshness; canonical evaluation does not.
- Canonical evaluation operates only on (known_trees, anchored_set) inputs.
- The prev field creates a per (tree, context, actor) hash chain.
- Signatures are verifiable against published public keys.
- Given identical (known_trees, anchored_set) inputs, evaluation produces identical state.
Any runtime satisfying these requirements is a valid IAM host.
The following concerns are explicitly outside iam-core 1.0 and belong to JAM runtime or higher layers:
Some of these may appear in future IAM versions. Most belong permanently to JAM runtime, where they can evolve independently of the constitution.
IAM defines:
- identity creation (name, incantation, root key derivation, seal)
- graph identification (bootstrap hash for community, root pubkey for personal)
- graph operations (ACCEPT, REVOKE, LEAVE)
- record structure (canonical JSON, id, prev chain, signatures)
- record authority (root keys sign structural records, actor must be currently valid)
- three layers: observation, admission, anchoring
- canonical evaluation (deterministic function over known_trees and anchored_set)
- topological traversal (prev as causality, (m, id) as tiebreaker)
- validity (structural and semantic, including cycle prevention and fork detection)
- multi-parent acceptance (DAG, not strictly tree)
- time (m, granularity, admission bounds, NOT a conflict judge)
- lineage (origin path)
- membership (evaluated presence at anchored state)
- genesis (bootstrap list creation, tree id as content hash)
- bootstrap inclusion (anchored set MUST contain bootstrap for every referenced tree)
- mortality (trees can die if bootstrap is lost; this is accepted, not a failure)
- security boundary (offline brute force is bounded only by incantation entropy)
- compromise recovery (no key rotation; recovery is via new identity and re-acceptance)
- phantom acceptance (ACCEPT is unilateral; phantom members are inert)
- axiomatic member semantics (bootstrap root can self-leave but cannot be externally revoked)
- known_trees registry as first-class input to evaluation
Everything else belongs to the runtime. IAM is the constitution: small, boring, and nearly immutable.
IAM does not define bridges to or from external identity systems (OAuth, OIDC, SAML, WebAuthn, or any other). Such bridges are runtime concerns and may be implemented as needed without modifying IAM core. An IAM identity and an OAuth account may coexist in the same application; they are never merged into a single key.
The canonical IAM wordlist contains exactly 256 words, indexed 0 through 255. Implementations MUST use this exact list:
amber anchor apex arch ash aspen atlas azure
bark basin beacon beam berry birch bison blade
bloom bolt bone boulder brass breeze briar bridge
brook calm canyon cape cedar chain chalk cipher
clay cliff clock cloud coal coast cobalt comet
coral cove crag crane creek crest cross crown
crystal curve cypress dagger dale dance dawn delta
depth dew dial dome dove draft drake dream
drift drum dune dusk eagle earth east echo
edge elder elm ember epoch fable falcon fawn
feather fern field finch fire fjord flame flare
flax fleet flint flora fog forge fossil fox
frond frost gable gale garnet gate gem ghost
glade glass glen globe gold grain granite grove
guild gull gust halo harbor hawk hazel heart
heath helm heron hill hive hollow honey horizon
horn hound hush ice inlet iron isle ivory
jade jasper jet jewel jungle juniper kelp kindle
knoll lake lance lark latch laurel leaf ledge
light lilac linden loam lodge lotus lunar lynx
maple marble marsh meadow mesa mint mist moon
moss muse myth north nova oak oasis oat
onyx orbit orchid otter palm path peak pearl
pine plume pond prairie prism pulse quartz quill
quiver rain raven reed reef ridge river robin
rock root rose rune rust saber sage sand
shard shell shore silk silver slate smoke snow
south spark spire spring star steel stone storm
sun surge swift thicket thorn tide timber tor
torch trail vale vault vine violet vista void
wake wave west wheat wild willow wind winter
wolf wood wren yarn yew zenith zephyr zero
iam-core 1.0