Skip to content

Announce does not bind a DID to its first-seen key, so any caller can rewrite any peer's http_url #273

Description

@beardthelion

upsert_peer has no notion of who owns a DID's row. Any caller can rewrite any peer's http_url, because the conflict branch is an unconditional update and the announce route accepts unsigned callers by default. #270 proposes resetting last_ping_ok when the URL changes, which limits the blast radius on one consumer. This issue is the underlying question: should a DID be bound to the key that first announced it, so the rewrite is rejected rather than merely defanged?

Split out of #270 so the contained mitigation there can land without waiting on this decision.

Why the mitigation is not sufficient on its own

The reset-on-change fix keys on last_ping_ok, but three of the four consumers of http_url never read that flag:

  • sync.rs:204 resolves a queued sync item's origin URL by DID and hands it to clone_repo/fetch_repo as the git remote. No reachability filter.
  • api/repos.rs:1350 fans out the post-receive announce to every peer with a non-empty http_url. No reachability filter.
  • api/resolve.rs:24 serves GET /api/v1/resolve/{did} from the peer table and returns the stored URL as the answer for that DID. No reachability filter, and the route is public (server.rs:381).
  • api/repos.rs:1451 is the only one that filters on last_ping_ok, and it is the one Unsigned announce repoints a peer's http_url and inherits its reachable=true federation gate #270's fix addresses.

So a repointed row keeps steering the sync worker, the notify fan-out, and public DID resolution regardless of what the flag says.

The decision

Options as I see them:

  1. First-writer-wins on the DID. Record the announcing key on insert; later announces for that DID must carry a signature from the same key. Closes the rewrite outright, works whether or not require_signed_peer_writes is on, and does not depend on any downstream consumer reading a flag. Cost is a migration plus a story for peers whose rows predate the column, and a documented recovery path for a genuine key rotation.
  2. Flip require_signed_peer_writes to true by default. Simpler, but it only requires a valid signature matching the announced DID, which is what an attacker announcing their own DID already has. It closes the hijack of someone else's row and nothing else, and it is a breaking change for peers that have not upgraded, which is why it is off today.
  3. Ship Unsigned announce repoints a peer's http_url and inherits its reachable=true federation gate #270's reset and accept the residual. Cheapest, and leaves the three unfiltered consumers exposed.

My read is that 1 is the actual fix and 3 is a stopgap worth landing first, but this is a federation-model call rather than a bug with an obvious patch, which is why it is its own issue.

Related

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    crate:nodegitlawb-node — the serving node and REST APIkind:securityVulnerability fix or hardeningsev:highMajor break or real security/trust risk, no easy workaroundsubsystem:apiNode REST API request/response surfacesubsystem:identityDID/UCAN, http-sig auth, push authorizationsubsystem:peersPeer announce, discovery, and registry

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions