Skip to content

Security: RingsNetwork/rings

SECURITY.md

Security And Overlay Threat Model

Reporting a Vulnerability

Report privately through GitHub's private vulnerability reporting or to dev@rings.rs, never in a public issue. Fixes ship in the next release; before 1.0 only the latest release is supported.

Summary

Every peer and message is authenticated by a DID and a delegated signing key. That proves control of a key, not that keys are scarce, so Sybil and eclipse resistance is not yet provided (see Non-Goals). The rule between the two layers is: the communication layer minimizes leakage; the privacy layer provides privacy.

Assumptions

  • A signature check authenticates control of a DID's key. It does not show that two DIDs belong to different operators.
  • An established WebRTC connection provides channel security.
  • Every signature is bound to its overlay's network_id and a per-message-family domain tag. network_id separates overlays; it is not a secret.
  • Honest peers run the protocol, refresh descriptors, and take part in stabilization and replication.

Fault Model

Churn and fail-stop faults are handled: peers disconnect, crash, restart, and miss heartbeats, and TTLs, stabilization, storage repair, and descriptor refreshes recover. Byzantine faults are attributable but not prevented: a peer can drop or delay messages, ignore its advertised policy, or withhold data, and every claim it makes is signed.

Deployment Models

Model Fit
Controlled membership: operators choose which keys join Supported
Authenticated open membership: anyone can create a DID Supported, with application allowlists or authorization on top of the per-origin bounds
Permissionless adversarial membership Not yet: needs ring positions that identities cannot choose (#780)

Layer Contracts

A property belongs to the communication layer only if the plain relay gives it to every message; anything that needs a circuit belongs to the privacy layer.

Communication layer

crates/core provides payload authenticity for every message, and confidentiality after the E2E handshake. Every hop, and the destination, learns:

  • the origin DID, from the transaction signature;
  • the destination DID, from which the next hop is chosen;
  • its own predecessor and successor;
  • the message's size and arrival time;
  • whether the route has crossed its aim, and nothing else about the route.

Unlinkability cannot be added here: Chord routes by destination, and the signature names the origin. Confidentiality is opt-in: a DID is a key digest, so a sender first runs the E2E handshake to learn the peer's key. Messages outside the E2E stream family are readable by every hop and by a storage owner holding them for an offline recipient.

Routes are loop-free and take at most 2|V| hops. While an origin has no predecessor, its requests may name one of its links in a signed reply_via, which receives at most one successor or connection answer and forwards it only over a direct link. The hop budget is unsigned: it bounds honest work, not a dishonest hop. A leak here is a communication-layer bug; circuits inherit whatever the relay exposes.

Privacy layer

crates/node/src/onion builds layered ElGamal-AEAD circuits over direct edges.

  • Per-hop knowledge. Each relay learns only its predecessor and successor; circuit ids are rewritten at every hop. Only the exit sees the payload; only the client knows the route. Routes have at most eight hops, three by default.
  • Cover and pacing. Every non-empty batch toward a next hop holds exactly 4 cells, padded with authenticated cover, after a 5–25 ms delay. Cells use fixed size classes from 4 KiB to 12 MiB. Idle links send nothing.
  • Replay. One (peer, circuit, nonce) authorizes at most one exit action, and each exit draws a fresh process epoch per start, which invalidates cells built before a restart.
  • Entry guards. A client pins a persisted set of first hops, limiting how many relays see its edge over time.

A circuit hides hops from each other and the client from the exit. It does not hide the client from its first hop, overlay membership, registry lookups, or timing from an observer of every link. Anonymity also depends on the candidate set: registry descriptors are signed and expire, but a party with many identities can try to hold several positions of a route.

Feature Boundaries

  • Replay. The destination keeps a persisted 32-sequence window per (network_id, origin, destination, traffic class) stream, so a transaction is dispatched at most once while the store is retained; a damaged record disables only its stream. Exactly-once effects are not claimed. Details.
  • Rate limits. After verification, the destination charges per-origin message and byte buckets keyed by the account DID, committed together with replay and before the application sees the message. Rotating keys or relays does not reset them, and a drop never disconnects the relay.
  • Application boundary. Core never calls, awaits or consults application code (#935): the application cannot see, validate or veto a protocol message, nor send one, and no inbound lane waits for it. A slow application therefore costs only its own messages, never a peer's credit or another link's liveness. Application delivery is lossy by design: a message that does not fit the full inbox is dropped and counted, unknown to its sender, so a protocol that needs a reliable stream detects loss end to end: the TCP relay resets a stream with a segment it cannot recover, and an E2E stream with a lost frame fails as stalled, so neither passes on a stream with a silent gap (#939). Relay mail leaves core's custody once the application takes it from the inbox; it is delivered at least once within a run, and a restart before its retirement loses it (#936). Details.
  • Measurement and credit. Local and advisory: they may reorder eligible candidates but never add one or change DHT placement. Service receipts affect neither.
  • Connection admission. Connection records are bounded at twice the topology's reference slots; at the bound, only peers no topology slot references can be evicted.
  • Control API. One owner-only Bearer token guards both JSON-RPC listeners; only the handshake methods nodeDid and answerOffer are public. The external listener binds a non-loopback address only on opt-in, and browser requests must come from configured origins.
  • DHT storage. Retention is capped at the maximum TTL; carriers are bounded in count and size; versions too far ahead of the receiver's clock are rejected. Each data-topic element expires at its own horizon, and a removal is collected only once every write it covers has expired everywhere within the clock-skew tolerance, so a removed value is not resurrected while the carrier is retained. A relay inbox is verified by its owner, readable only by its recipient, and removable only by its recipient or, in an ownership hand-off, by the previous owner once the next one acknowledged holding exactly that value; it is capped at 64 messages. Values are stored in the clear. Details.
  • Transport flow control. Every lane of a connection is credit flow controlled: a receiver refuses no honest frame, refuses and reports a frame beyond its credit, and pushes overload back to the sender. One peer's backpressure never holds another's control lane (lane 0, which the node-wide receive bound never defers; that bound has no per-peer share, so one peer's load can defer the other lanes of every link). Liveness reads no credit state, so a peer that withholds the control lane's credit is evicted whatever else it sends. The node-wide receive bound is soft (#934). Details.
  • Delegation references. A link references a delegation only under a bounded lease its receiver granted to a frame that verified, sent as a remaining duration, so no two clocks are compared. The receiver pins at most 128 delegations per link, 8 of them reserved for the link peer's own, and refuses a new lease while every slot is still in its grace; the sender then sends inline. A frame whose reference misses is dropped uncharged, never held, so a lane's frames reach admission in arrival order (#908). An origin delegation that expired in flight is not charged to the hop that carried it, and only when the rest of the frame verifies. An unrelated party cannot populate a link's tables. Details.
  • Wire decoding. Every decoder that admits relayed bytes has generated malformed-input tests in its crate.
  • Native gateway. A TUN gateway starts only on enabled: true or --gateway, captures nothing until the operator lists a prefix, and never grants itself host capabilities.

Non-Goals

  • Sybil and eclipse resistance. DIDs stay key digests for Ethereum and Solana addressing, so the planned defence removes position choice instead of pricing identities: pos(did, e) = H(did, beacon(e)) (#780).
  • Unlinkability on the communication layer.
  • Resistance to a global traffic observer.
  • Availability against Byzantine storage owners or route candidates.

Known Limitations

  • #912: the browser replay store uses IndexedDB's default durability hint.
  • #915: a missing replay record restarts its stream from the first sequence.
  • #916: durable replay writes cap a node near 50 transitions per second on macOS.
  • #918: registry tombstones are bounded by the heartbeat rate per registrant, not by count (bounds).
  • #920: storage byte-budget eviction can drop a removal before it is collected, resurrecting the values it covered.
  • #921: data-topic tombstones are bounded by rate, not by count or bytes, and a forged removal received by sync can hold a topic live for up to the horizon past the receiver's clock.
  • #932: a reassembled message whose full-size mailbox charge does not fit is dropped.

There aren't any published security advisories