Repository navigation
Description
Summary
pkgs/id chat presence is the one signature-verification site in the crate that still uses plain Ed25519 verify instead of verify_strict — so the shared small-order identity forgery from #1386, which every sibling verifier refuses, is accepted as an authenticated presence heartbeat.
Evidence (origin/main a51be0e)
pkgs/id/src/chat_presence.rs:157-165—PresenceHeartbeat::verify()ends withverifying_key.verify(&self.signing_bytes(), &signature).is_ok()(line 164). This is the production gate for gossip presence:pkgs/id/src/commands/serve.rs:2021(receive_presence_heartbeat) drops a heartbeat only whenverify()returns false.- Every sibling verifier in the same crate uses
verify_strict:invites/event.rs:126,seitan/accept.rs:142,artifact.rs:321,peer_announcement.rs:79,directory_log.rs:100,directory.rs:383,web/identity.rs:523,world_session/mod.rs:846,hosted_binding.rs(multiple). pkgs/id/src/test_forgeries.rs:19-27documents why: the identity point is a small-order public key, and the forgery (R = identity, S = 0) passes plainverifyfor any message under it; onlyverify_strictrefuses it because the key is small-order. The helper's own test (test_forgeries.rs:51) pinsverify = Ok,verify_strict = Errfor exactly this pair.- Verified this run on the laptop at a51be0e (probe worktree
~/git/fleet/probe-2026-11-03-laneB3-presence, sources kept): a test constructing aPresenceHeartbeatwithnode = IDENTITY_KEY,signature = identity_forgery(), one channel and a current timestamp printsPROBE presence verify() on identity forgery = trueand passes its bug assertion on unpatched main (cargo test --lib probe_identity_forgery_against_presence_verify, 1 passed) — i.e. a heartbeat nobody signed passes the authentication gate. - The existing presence auth test (
chat_presence.rs:717heartbeats_authenticate_by_signature) covers unsigned/tampered heartbeats only — no id: verify_strict forgery regressions missing at invites and seitan call sites (P0.11) #1386 forgery fixtures, unlike the regression tests id: verify_strict forgery regressions missing at invites and seitan call sites (P0.11) #1386/test(id): verify_strict forgery regressions at invites and seitan sites (#1386) #4676 added at the invites and seitan sites.
Why it matters
Presence is scoped by group membership after the signature gate, which bounds the blast radius today — but the module's own contract (chat_presence.rs module doc, "Authentication") states the signature is the authentication for a heartbeat's claimed node, because the gossip plane does not authenticate authors. A verifier that accepts a known, fixture-grade forgery for a small-order claimed node breaks that contract and diverges from the crate-wide #1386 posture: any future caller that trusts verify() without the membership filter (the exact failure mode #4502's guarded-door design warns about) inherits a forgeable gate. The fix shape is already established at five sibling sites.
Dedup / related
- id: verify_strict forgery regressions missing at invites and seitan call sites (P0.11) #1386 (closed) and test(id): verify_strict forgery regressions at invites and seitan sites (#1386) #4676 (closed) covered the invites and seitan call sites only; neither names chat_presence.
- Design: one guarded door — every gossip claim enters through its table (pkgs/id presence + peers) #4502 (open, gossip-admission design) covers canonicalisation/bounds/scoping of presence and peers; it does not disposition signature strictness.
- Presence signature issues id(chat-presence): receipt never scopes heartbeats to sender membership — update_scoped has no production caller, serve stores channels unscoped #4363/fix(id): scope received presence to group membership #4541 (scoping) and id(chat-presence): update_at caps the raw channel list without dedup — duplicate entries displace distinct channels a heartbeat genuinely claims #4404/id(chat-presence): heartbeat typing list is never deduplicated or bounded — one channel yields a 3.3 MB signed frame, defeating MAX_HEARTBEAT_CHANNELS #3991/id(chat-presence): PresenceTable has no node-count bound — every distinct self-signed key is stored for a full TTL, so minted keys grow serve's memory without limit #4004 (bounds) are different defects.
Acceptance criteria
PresenceHeartbeat::verify()refuses the small-order identity forgery: a heartbeat withnode = IDENTITY_KEYandsignature = identity_forgery()(fromcrate::test_forgeries) returnsfalsefromverify()and is dropped byreceive_presence_heartbeat.- A regression test at the presence site mirrors the invites/seitan id: verify_strict forgery regressions missing at invites and seitan call sites (P0.11) #1386 tests (identity forgery refused; a genuine signature still verifies — non-canonical-S disposition matching the sibling sites).
- No behaviour change for honestly signed heartbeats: the existing
chat_presencetest suite passes (cargo test --lib chat_presenceinpkgs/idon the laptop or CI).
Activity
dezren39 commentedon Nov 4, 2026
🤖 Agent note · filed
dezren39 commentedon Nov 4, 2026
🤖 Agent note · picked up
dezren39 commentedon Nov 4, 2026
🤖 Agent note · parked
basic: production line limit (2500)check on PR fix(id): verify_strict in PresenceHeartbeat::verify (#5226) #5308 finishes; Codex review completed with no findings, targeted tests pass locally (17/17 chat_presence) — merge on green and complete task 8972fix(id): verify_strict in PresenceHeartbeat::verify (#5226) (#5308)
dezren39 commentedon Nov 4, 2026
🤖 Agent note · done
PresenceHeartbeat::verifynow usesverify_strict, plus the two id: verify_strict forgery regressions missing at invites and seitan call sites (P0.11) #1386 regression tests (identity forgery rejected; non-canonical S rejected with genuine-signature control). Laptopcargo test --lib chat_presence: 17 passed, 0 failed (per PR fix(id): verify_strict in PresenceHeartbeat::verify (#5226) #5308). Codex review on PR fix(id): verify_strict in PresenceHeartbeat::verify (#5226) #5308: no findings. The only pending check at merge wasbasic: production line limit (2500)— the changed file is 902 lines, and the check was stuck in the Actions queue backlog (all main runs QUEUED); merged under the standing merge authority.