Skip to content

release: v0.15.0 — federation event signing#127

Merged
Fail-Safe merged 9 commits into
mainfrom
next
Jun 11, 2026
Merged

release: v0.15.0 — federation event signing#127
Fail-Safe merged 9 commits into
mainfrom
next

Conversation

@Fail-Safe

Copy link
Copy Markdown
Owner

Authenticate federated events with per-cortex Ed25519 signatures so a shared-key ring no longer has to trust any key-holder to author events as any cortex.

  • Signed events: noema keygen gives a cortex an Ed25519 key; it signs every event it emits and advertises the public key via the cortex_identity handshake.
  • Opt-in verification: federation.verify = off (default) | warn | enforce. Under enforce, replay rejects events not correctly signed by their owning cortex, and source-locking is enforced on the replay path.
  • Key trust: trust-on-first-use per cortex_id, with conflict detection and downgrade resistance; optional out-of-band hard-pin (pubkey: on a peer entry); rotation recovery via reset-peer --key-rotated.
  • Hardening: the 0600 seed sidecar is written atomically and gitignored; migration 017 adds additive signature/pubkey columns so mixed-version rings degrade gracefully.
  • Fixes: the copied-directory guard no longer false-flags a federated receiver cortex.
  • Docs: contributor docs moved to AGENTS.md + docs/; Obsidian plugin bump.

Verify defaults to off, so upgrading changes nothing until an operator opts in.

Fail-Safe and others added 9 commits June 4, 2026 21:26
Format migration list in CLAUDE.md for readability
Remove references to deleted design documentation files
Document --limit flag for embeddings backfill command
Add regression coverage for the federation event-signing feature:

- migration017_test.go: assert the signature/pubkey columns exist as
  NOT NULL DEFAULT '' and that a row inserted without them backfills to
  the empty (unsigned) sentinel rather than NULL, the property a
  mixed-version ring relies on to treat pre-signing events as unsigned.

- signing_interop_test.go: exercise the full cross-cortex wire path the
  in-process unit tests skip — emit on cortex A, JSON-serialize exactly
  as sync_events transmits, deserialize, and ReplayEvent on cortex B.
  Covers signed round-trip under enforce (with TOFU pin), in-transit
  tamper rejection, the mixed-version unsigned-upstream matrix
  (enforce rejects / off accepts), and no-silent-downgrade-after-pin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign every emitted event with a per-cortex Ed25519 key and verify
replayed events against the originating cortex's pinned key, moving the
federation trust anchor from the shared transport key to the cortex that
authored each event. Once cortex_id is authenticated, source-locking is
enforced on the replay path: a source-locked trace may only be mutated by
its owning cortex.

- eventsig: canonical, length-prefixed, domain-separated preimage with
  sign/verify and ed25519:<base64> key+signature encoding.
- noema keygen generates/rotates the key into a 0600 sidecar and records
  the public key in cortex.md; the cortex_identity handshake advertises it.
- Key distribution is trust-on-first-use keyed on cortex_id, with conflict
  detection and downgrade resistance. A high-assurance peer's key can be
  hard-pinned via pubkey: on its cortex.md entry, which overrides TOFU and
  refuses a mismatching handshake regardless of verify mode.
- Verification is staged via federation.verify (off default | warn |
  enforce) so a mixed-version ring never hard-partitions; migration 017
  adds the additive, NOT NULL DEFAULT '' signature/pubkey columns.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the root CLAUDE.md with AGENTS.md and move reference material into
docs/architecture.md and docs/development.md, linked from the README.
Update the Makefile and a migration comment that pointed at CLAUDE.md, and
ignore the generated docs/layout.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make explicit in the federation signing section that off (default) checks
nothing and warn logs but still applies forged events, so the forgery and
source-lock-bypass risks stay open until peers are moved to enforce.
off/warn are a rollout on-ramp, not protection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the O_TRUNC-then-write with a temp-file + fsync + rename so a
crash or error mid-write can never truncate or partially overwrite an
existing key. A key clobbered halfway through a `keygen --force` rotation
would otherwise force every peer to re-pin. The temp file sits beside the
target to keep the rename on one filesystem, and the 0600 mode is forced
explicitly so it holds even over a looser-permission original.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the seed file to a .gitignore at the cortex root so it can't be
accidentally committed if the cortex directory is (or becomes) a git
repo. Idempotent, creates .gitignore if absent, and skips a key stored
outside the cortex directory (which a .gitignore there couldn't cover).
A gitignore failure warns but does not fail keygen — the key is valid
either way. Also refresh the now-stale O_TRUNC comment on the write path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
detectCopiedDirectory keyed on "any foreign cortex_id with none of our
own" — which is the normal state of a receiver/subscribe cortex after its
first federation sync, since peer events are replayed under the originating
cortex's id. The guard runs on every Open, so it made such a cortex
unopenable: serve restart and every CLI command (including the reset-peer
recovery the signing feature points operators to) failed with "appears to
be a copy", and the suggested escape hatch (migrate cortex-id --reset)
didn't even help — it only re-keys rows this cortex authored.

Scope the check to locally-authored history: trip only when events with
origin == this cortex's name are recorded under a cortex_id other than the
one cortex.md declares — the actual copy/re-id signature, and exactly the
rows --reset re-keys. Peer-replayed events no longer count.

Pre-existing since the cortex-ULID work; surfaced by end-to-end federation
testing of the signing feature.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A key rotation (noema keygen --force) retires the old key, but a peer's
pre-rotation events stay signed with it. A full reset-peer resets the
cursor to the start, so the peer re-pulls those old-key events and, under
verify=enforce, rejects them (their embedded pubkey conflicts with the
newly re-pinned key) — the cursor sticks and recovery stalls.

Add reset-peer --key-rotated: clear only the pinned signing key, keeping
the cursor, pinned identity, and vclock. An already-caught-up peer then
re-pins the new key on its next handshake and pulls only post-rotation
events, never re-touching the retired-key history.

Document the residual limitation: a from-scratch resync of a rotated
cortex (a brand-new peer, or a full reset) still can't replay its
pre-rotation events under enforce — there is no key history. Rotate only
when forfeiting that to zero-state peers is acceptable.

Surfaced by end-to-end federation testing of the signing feature.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Fail-Safe Fail-Safe merged commit 65c4b04 into main Jun 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant