Skip to content

Withdrawal tombstones an identity the graph does not hold #1116

Description

@prk-Jr

Description

On a consent withdrawal, ec_finalize_response writes a tombstone row for whatever identifier the request's ts-ec cookie names. write_withdrawal_tombstone performs an unconditional write_entry(..., Overwrite) in crates/trusted-server-core/src/ec/kv.rs, and the cookie is only shape-checked — is_valid_ec_id verifies 64 hex, a dot, and 6 alphanumerics, with no signature recomputation.

The tombstone exists to stop later reads of a real row by batch-sync clients during the 24-hour revocation window. Writing one for an identifier the graph never held enforces nothing, while still consuming a write and occupying a keyspace entry with a TTL. Since the identifier arrives in a client-supplied cookie, that write is the client's to trigger, and the random 6-character suffix is not covered by the HMAC, so each variation is a distinct row.

Recomputing the HMAC does not address it: the suffix is unsigned, so it can be varied freely once a valid prefix is known, and a user whose IP changed between issuance and withdrawal would fail the check and have a genuine opt-out discarded.

Determining existence needs care in both directions:

  • A Fastly lookup is eventually consistent, so a stale "missing" would discard a withdrawal for an identity issued moments earlier.
  • Counting keys by prefix is not an existence check — any longer key starting with the identifier answers for it.
  • If existence cannot be determined at all, writing regardless restores the unconditional write whenever the store can be made to fail.

Separately, log_id truncates by byte with get(..8).unwrap_or(ec_id). When byte 8 splits a character the fallback emits the whole identifier, and the Fastly lookup error embedded the full key in its message.

Expected behavior

  • Only an identity the graph holds is tombstoned.
  • Existence is determined exactly and with a strongly consistent read.
  • A store that cannot answer never causes a write for an identity that was never issued, and never silently discards a genuine withdrawal.
  • The browser cookie is expired in every case — it is the primary enforcement.
  • A redacted identifier is never emitted in full.

Affected area

Core (Edge Cookies, GDPR)

Done when

  • A withdrawal for an identity the graph does not hold creates no row
  • A genuine withdrawal still tombstones, verified through the full request path
  • Existence is exact, not a prefix count, and pages are followed to exhaustion
  • An unanswerable check writes nothing and is reported once with its reason
  • Cookie expiry and header removal survive a store outage
  • Redaction holds regardless of the identifier's encoding

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions