Skip to content

Auction accepts spoofed bids/receipts — bidder_peer_id/worker_peer_id never checked against gossip source #9

Description

@eltociear

Summary

The auction's buyer-side code (node/crates/c0mpute-core/src/buyer.rs, run_auction()) accepts JobBid and JobReceipt gossip messages based only on the self-reported bidder_peer_id / worker_peer_id string fields inside the JSON payload. It never checks these against GossipMessage.source (Option<PeerId>), which is the actual libp2p-authenticated sender identity already attached to every received message.

Since PeerId is just a string a bidder or worker puts in their own JSON payload, any peer on the network can claim to be any other peer_id in a JobBid or JobReceipt — there is currently nothing stopping it.

Where

  • Bid loop: buyer.rs lines ~122-136 — matches on bid.job_id == job_id && bid.bidder_peer_id != offer.buyer_peer_id, no source check.
  • Receipt loop: buyer.rs lines ~191-208 — matches on receipt.job_id == job_id && receipt.worker_peer_id == winner.bidder_peer_id, no source check.

Compare with capabilities.rs (node/crates/c0mpute-core/src/capabilities.rs, around line 172), which already does this correctly for capability ads: it discards any ad with msg.source == None and keys its registry by the verified source, not by the self-reported peer_id field inside the ad payload.

Impact

  1. JobAccept (naming the winning bidder + agreed_price_usd) is broadcast publicly on the job's gossip topic — every subscribed peer, not just participants, can see who won a given job and for how much.
  2. Any peer can then publish a JobReceipt with worker_peer_id set to the winner's peer_id and a fabricated output_hash/status: Completed, before the real winner finishes the work.
  3. run_auction() returns on the first message matching job_id + the claimed peer_id string, so whichever receipt arrives first — forged or genuine — wins. This means either: (a) a forger can get an AuctionOutcome credited to the real worker's identity for work never actually performed, or (b) at minimum it griefs the real worker, since their later genuine receipt is simply never observed (the function already returned).
  4. The same spoofing applies to bids: an attacker can publish a lowball bid under a different peer's bidder_peer_id, get it selected as the winner, and then never deliver — while the real bidder with that identity never placed that bid.

JobReceipt.signature is documented as "Signed by the worker's CoinPay DID. Verified by buyer + validator" but is never actually checked anywhere in the codebase (confirmed via grep -rn "\.signature" node/crates — no hits outside the unrelated secure-chat module). That's presumably tracked as future work once CoinPay's DID signing ships. This report is about something available today, independent of that: GossipMessage.source is already a strongly-authenticated value (derived from the peer's actual keypair at the libp2p transport layer) and costs nothing to check — it's just not being checked.

Suggested fix (PR opened)

Before accepting a bid or receipt, verify msg.source.map(|p| p.to_base58()) == claimed_peer_id. Discard (log + skip) on mismatch, exactly like capabilities.rs already discards ads with no verifiable source.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions