Skip to content

fix(core): verify federation peer certificates against a trusted allowlist - #39

Merged
Mearman merged 5 commits into
mainfrom
fix/federation-auth-hole
Sep 9, 2026
Merged

fix(core): verify federation peer certificates against a trusted allowlist#39
Mearman merged 5 commits into
mainfrom
fix/federation-auth-hole

Conversation

@Mearman

@Mearman Mearman commented Sep 9, 2026

Copy link
Copy Markdown
Member

Fixes #38.

Federation links accepted any certificate on either side, and nothing in the shipped product accepted an inbound federation connection at all — three gaps named as P0 in #37 and never actually closed.

What changed

  • FederationManager gets a per-instance trusted-fingerprint allowlist, empty by default (federate with nobody until an operator explicitly pins a remote mesh's fingerprint — the same no-CA, pin-the-key trust model ordinary peer connections already use).
  • Both connect() (outbound) and handleInbound() (inbound) verify the peer's presented certificate against that allowlist before a link is created. An unpinned certificate gets the socket destroyed before any handshake is processed — no state sync, no ready link.
  • listen()/stopListening(): a real production TLS server for inbound federation connections, replacing the fact that handleInbound() previously had no production caller at all — only the integration test exercised it, against its own hand-rolled tls.createServer.
  • CommsStore gains getFederationFingerprint, fedTrust/fedUntrust/fedTrustedFingerprints, and fedListen/fedStopListening, implemented on MeshStore and stubbed not-supported on FileStore.
  • New MCP actions (mesh_fed_fingerprint, mesh_fed_trust, mesh_fed_untrust, mesh_fed_trusted, mesh_fed_listen, mesh_fed_stop_listening) so an agent can actually drive the trust flow: read its own fingerprint, hand it to the other side out of band, pin what comes back, then connect.
  • The integration test now goes through the real fedListen/fedTrust path instead of the hand-rolled server, and gains a case confirming an unpinned connection is rejected outright. It's now wired into package.json as test:federation, included in test:all — previously nothing ran it, which is how the unwired listener went unnoticed this long.

Verification

pnpm typecheck, pnpm lint, pnpm build, pnpm test (23/23), pnpm test:delivery (7/7), and pnpm test:federation (including the new rejection case) all pass locally.

Deliberately out of scope

While tracing this down I found the same gap one level deeper: ordinary (non-federation) peer connections also never verify a connecting peer's certificate against its claimed peerIdgrep -r getPeerCertificate src/ returns nothing outside this PR. tls-transport.ts has a comment claiming fingerprint verification happens post-handshake; it doesn't. That's a distinct, arguably more significant issue affecting the default mesh path rather than the optional federation feature, and fixing it means touching the core peer-connection flow every bridge relies on — deliberately not folded into this PR. Will file separately.

FederationManager accepted any certificate on inbound or outbound TLS
connections — rejectUnauthorized: false is required since these are
self-signed with no CA, but nothing verified which self-signed cert
was presented, so any certificate was treated as a valid federation
peer.

Add fingerprintDer(), which hashes a live tls.PeerCertificate's raw
DER the same way getCertificateFingerprint() hashes a PEM certificate,
so a fingerprint pinned from one form compares equal to the other
presented live over a socket.
…owlist

Neither direction of a federation link checked the peer's certificate
fingerprint: connect() accepted whatever the remote server presented,
and handleInbound() accepted whatever the remote client presented and
immediately synced full mesh state (every visible agent, every
federated room's membership) to it. Federation trusted nobody in
particular and everybody at once.

Add a per-instance trusted-fingerprint allowlist (empty by default —
federate with nobody until an operator explicitly pins a remote
mesh's fingerprint, the same no-CA pin-the-key model ordinary peer
connections already use) and verify the presented certificate against
it before a link is created in either direction. An unpinned
certificate gets the socket destroyed before any handshake is
processed.

Also add listen()/stopListening(): a real production TLS server for
inbound federation connections. Nothing previously stood one up —
handleInbound() existed only as a function the integration test
called against its own hand-rolled tls.createServer.
Extend the CommsStore interface with the operations needed to
actually use federation's new fingerprint allowlist: getFederationFingerprint,
fedTrust/fedUntrust/fedTrustedFingerprints, and fedListen/fedStopListening.

MeshStore delegates to the corresponding FederationManager methods.
FileStore, which doesn't support networking at all, follows its
existing not-supported pattern for the mutating operations and
returns empty/no-op results for the read-only ones.
mesh_fed_fingerprint, mesh_fed_trust, mesh_fed_untrust,
mesh_fed_trusted, mesh_fed_listen, and mesh_fed_stop_listening give an
agent the operations needed to actually establish a federation link
under the new fingerprint-verification requirement: read this
instance's own fingerprint to hand to the other side, pin the other
side's fingerprint once received out of band, and start accepting
inbound links.
…in CI

Rewrite the federation integration test to establish links through
the real production path (store.fedListen(), replacing the test's own
hand-rolled tls.createServer) and to pin fingerprints on both sides
before connecting, matching the new verification requirement. Add a
case confirming an inbound connection with no pinned fingerprint is
rejected and creates no link.

Wire the test into package.json as test:federation, included in
test:all — previously nothing ran it at all, which is how
handleInbound() went unwired for this long without anything catching it.
@Mearman
Mearman marked this pull request as ready for review September 9, 2026 16:22
@Mearman
Mearman merged commit 6790898 into main Sep 9, 2026
5 checks passed
@Mearman
Mearman deleted the fix/federation-auth-hole branch September 9, 2026 16:23
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 1.27.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review ⚠️ Failed 2026-09-09T16:50:17.248976Z 9740d8c Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Federation links accept any certificate, and nothing calls the inbound handler in production

1 participant