Skip to content

id(chat-presence): PresenceHeartbeat::verify uses plain verify — the #1386 small-order identity forgery passes the presence authentication gate #5226

Closed

Description

@dezren39

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 with verifying_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 when verify() 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-27 documents why: the identity point is a small-order public key, and the forgery (R = identity, S = 0) passes plain verify for any message under it; only verify_strict refuses it because the key is small-order. The helper's own test (test_forgeries.rs:51) pins verify = Ok, verify_strict = Err for 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 a PresenceHeartbeat with node = IDENTITY_KEY, signature = identity_forgery(), one channel and a current timestamp prints PROBE presence verify() on identity forgery = true and 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:717 heartbeats_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

Acceptance criteria

  • PresenceHeartbeat::verify() refuses the small-order identity forgery: a heartbeat with node = IDENTITY_KEY and signature = identity_forgery() (from crate::test_forgeries) returns false from verify() and is dropped by receive_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_presence test suite passes (cargo test --lib chat_presence in pkgs/id on the laptop or CI).

Activity

  1. added
    kind/securitySecurity issue or security hardening
    task/small-fixQueue packet kind: bounded fix with reproduction and targeted tests
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    on Nov 4, 2026
  2. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 4, 2026
  4. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  5. added
    queue/needs-reviewQueue task finished and is awaiting review
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 4, 2026
  6. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  7. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/needs-reviewQueue task finished and is awaiting review
    on Nov 4, 2026
  8. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

  9. removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    work/assignedQueue worker lifecycle state
    on Nov 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)kind/securitySecurity issue or security hardeningpriority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions