arborist/aborist/mesh/members.py
russell@unturf.com aa8caeeece
mesh: cryptographic foundation, off by default
Phase 1 of the federation/gossip layer fox sketched as the natural
extension of v9.8 admissibility's content-addressed identity. Two
peers ingesting the same dump already compute identical document_roots
and identical 8-dim cache_keys; the mesh layer is the wire-and-trust
plumbing that lets them dedup answers, exchange Merkle proofs, and
cleanly distrust an evicted member without a hard fork.

Cryptography (cryptography lib, audited):
  Ed25519       — every membership mutation + (future) gossip envelope
                  is signed by the actor's pubkey.
  X25519 ECDH   — wraps each epoch's symmetric mesh secret to every
                  current member's DH pubkey via HKDF-derived AEAD key.
  ChaCha20-P1305— AEAD for envelope payloads + per-member secret wrap.

State machine:
  mesh_identity   — singleton; this peer's keys + group name
  mesh_roster     — per-epoch (member_id, sign_pub, dh_pub, role)
  mesh_epochs     — epoch_id -> {started_at, started_event_hash,
                                  secret_envelope JSON, reason}
  meta:mesh.enabled flag — off by default; gates everything

Eviction works by rotating to a new epoch whose envelope omits the
kicked member. Their prior signatures stay verifiable (the older
roster row is retained), but any gossip from epoch+1 onward is
opaque to them — the secret was never shared with their pubkey.

Authority gate: only roster members with role='admin' can add or
kick. Self-kick is rejected explicitly. The last admin can't be
kicked. Schedule-rotate (refresh secret, no roster change) is open
to any current member as a session-hygiene op.

Audit-chain integration: every mesh state mutation writes an audit
event (mesh_init, mesh_enable/disable, mesh_epoch_rotate). The
epoch's started_event_hash backfills into mesh_epochs after the
audit row commits, giving each epoch a tamper-evident pin into the
ledger.

CLI subcommands: mesh init, mesh status, mesh enable, mesh disable,
mesh members, mesh add, mesh kick, mesh rotate. All read-only or
local-state-only — no network code paths in this commit.

The HTTP gossip wire (`mesh sync`, `mesh serve`) is the next phase.
Schema, cryptography, and roster state machine are all in place to
support it without further migration.
2026-04-27 19:00:24 -04:00

135 lines
4.3 KiB
Python

"""High-level mesh workflows: join, kick, rotate.
These call into `state` (which handles DB writes + audit chain) and
verify authority before mutating roster. An admin-role member is
required to add or kick. Self-rotate (refresh secret without changing
roster) is open to any current member; this is a session hygiene op.
"""
from __future__ import annotations
import sqlite3
from aborist.mesh.state import (
MeshRosterEntry,
current_epoch,
load_identity,
roster_at,
rotate_epoch,
)
def add_member(
conn: sqlite3.Connection,
*,
member_id: str,
sign_pub: bytes,
dh_pub: bytes,
role: str = "member",
) -> int:
"""Admin-only. Add a new member, bumping the epoch.
The new envelope wraps a fresh secret to every member of the new
roster — including the new joiner — so they can decrypt all gossip
from this epoch forward. Prior epochs' secrets remain unrecoverable
by the joiner unless they can independently reconstruct an earlier
envelope (they can't, by design).
"""
me = load_identity(conn)
if me is None:
raise RuntimeError("mesh not initialized")
epoch = current_epoch(conn)
if epoch is None:
raise RuntimeError("no epoch state; call init_identity first")
_require_admin(conn, epoch, me.member_id)
existing = roster_at(conn, epoch)
if any(m.member_id == member_id for m in existing):
raise ValueError(f"member {member_id!r} already in roster")
new_roster = existing + [
MeshRosterEntry(
member_id=member_id,
sign_pub=sign_pub,
dh_pub=dh_pub,
role=role if role in ("admin", "member") else "member",
)
]
return rotate_epoch(
conn,
new_members=new_roster,
reason=f"join:{member_id}",
actor_member_id=me.member_id,
)
def kick_member(
conn: sqlite3.Connection,
*,
member_id: str,
reason: str,
) -> int:
"""Admin-only. Remove a member from the roster, bumping the epoch.
The kicked member's prior signatures remain verifiable forever (their
roster row at older epochs is preserved). They have no entry in the
new envelope, so they can't decrypt anything from epoch+1 onward.
"""
me = load_identity(conn)
if me is None:
raise RuntimeError("mesh not initialized")
epoch = current_epoch(conn)
if epoch is None:
raise RuntimeError("no epoch state")
_require_admin(conn, epoch, me.member_id)
if member_id == me.member_id:
raise ValueError("cannot kick yourself; use 'mesh disable' instead")
existing = roster_at(conn, epoch)
if not any(m.member_id == member_id for m in existing):
raise ValueError(f"no such member: {member_id!r}")
new_roster = [m for m in existing if m.member_id != member_id]
if not any(m.role == "admin" for m in new_roster):
raise ValueError(
"kick would leave roster with no admin; promote someone first"
)
return rotate_epoch(
conn,
new_members=new_roster,
reason=f"kick:{member_id}:{reason}",
actor_member_id=me.member_id,
)
def scheduled_rotate(conn: sqlite3.Connection, *, reason: str = "scheduled") -> int:
"""Refresh the epoch secret without changing the roster.
Open to any current member. Useful for periodic rekey hygiene or
after suspicion of secret compromise without an identifiable bad
actor (every member's keys still trusted).
"""
me = load_identity(conn)
if me is None:
raise RuntimeError("mesh not initialized")
epoch = current_epoch(conn)
if epoch is None:
raise RuntimeError("no epoch state")
if not any(m.member_id == me.member_id for m in roster_at(conn, epoch)):
raise PermissionError("not a member of the current epoch")
new_roster = roster_at(conn, epoch)
return rotate_epoch(
conn,
new_members=new_roster,
reason=f"rotate:{reason}",
actor_member_id=me.member_id,
)
def _require_admin(
conn: sqlite3.Connection, epoch: int, member_id: str
) -> None:
rows = roster_at(conn, epoch)
me = next((m for m in rows if m.member_id == member_id), None)
if me is None:
raise PermissionError(f"{member_id} not in epoch {epoch} roster")
if me.role != "admin":
raise PermissionError(f"{member_id} is not an admin in epoch {epoch}")