Sign-up has no deliberate backstop, and proof-of-work cannot simply be switched on #52

Closed
opened 2026-09-05 00:21:21 +00:00 by paulhitt · 2 comments
paulhitt commented 2026-09-05 00:21:21 +00:00 (Migrated from gitlab.com)

Split out of relay#9, which recorded that raising the sign-up limit from 5/IP/hour to 50 removed an accidental backstop and nothing deliberate replaced it. That is still true, measured 2026-09-04: no FN_SIGNUP_* is set in production at all.

The designed backstop exists and is off

FN_SIGNUP_POW_BITS gates sign-up behind a proof-of-work challenge. It is the right shape for this problem — it costs an attacker CPU per account while barely touching one person creating one account, and unlike a per-IP limit it does not punish everybody behind a shared NAT, which is why the 5/hour limit was raised in the first place.

🔴 Why it cannot just be enabled

RoutesAccount.swift:173 is unconditional:

if signupProofOfWorkBits > 0 {
    guard let challenge = input.challenge, let nonce = input.nonce else {
        throw WireError("challenge_required", ...)

Any client that does not send a challenge and nonce is refused outright. So turning this on breaks sign-up for every already-published build that lacks proof-of-work support — and it would present to a new user as the app simply not working, with no way for them to tell why.

What would make it safe

Some combination of:

  • confirm which shipped builds carry SignupProofOfWork (Android has it; iOS has it on main since the reinstate stack landed 2026-09-04, but has not shipped);
  • make enforcement conditional on the client advertising support, so an older build degrades to the rate limit rather than to a wall;
  • or hold it until the shipped floor on both platforms supports it, and say so.

⚠️ Enforcement being unconditional is not obviously wrong — a backstop a client can opt out of is not a backstop. That is exactly why this needs a decision rather than a flag flip.

Meanwhile

The other half of relay#9 is done: LiveTarget.refuseProduction throws if a live suite is pointed at production, and LiveTargetTest covers it (production hosts are refused / the disposable fleet and local relays are allowed). So the path that created those accounts is closed even with no rate-limit backstop.

Split out of relay#9, which recorded that raising the sign-up limit from 5/IP/hour to 50 removed an *accidental* backstop and nothing deliberate replaced it. That is still true, measured 2026-09-04: no `FN_SIGNUP_*` is set in production at all. ## The designed backstop exists and is off `FN_SIGNUP_POW_BITS` gates sign-up behind a proof-of-work challenge. It is the right shape for this problem — it costs an attacker CPU per account while barely touching one person creating one account, and unlike a per-IP limit it does not punish everybody behind a shared NAT, which is why the 5/hour limit was raised in the first place. ## 🔴 Why it cannot just be enabled `RoutesAccount.swift:173` is unconditional: ```swift if signupProofOfWorkBits > 0 { guard let challenge = input.challenge, let nonce = input.nonce else { throw WireError("challenge_required", ...) ``` Any client that does not send a challenge and nonce is refused outright. So turning this on **breaks sign-up for every already-published build that lacks proof-of-work support** — and it would present to a new user as the app simply not working, with no way for them to tell why. ## What would make it safe Some combination of: - confirm which shipped builds carry `SignupProofOfWork` (Android has it; iOS has it on main since the reinstate stack landed 2026-09-04, but has not shipped); - make enforcement conditional on the client advertising support, so an older build degrades to the rate limit rather than to a wall; - or hold it until the shipped floor on both platforms supports it, and say so. ⚠️ Enforcement being unconditional is not obviously wrong — a backstop a client can opt out of is not a backstop. That is exactly why this needs a decision rather than a flag flip. ## Meanwhile The other half of relay#9 is done: `LiveTarget.refuseProduction` throws if a live suite is pointed at production, and `LiveTargetTest` covers it (*production hosts are refused* / *the disposable fleet and local relays are allowed*). So the path that created those accounts is closed even with no rate-limit backstop.
paulhitt commented 2026-09-07 12:28:08 +00:00 (Migrated from gitlab.com)

mentioned in merge request !74

mentioned in merge request !74
paulhitt commented 2026-09-07 16:31:05 +00:00 (Migrated from gitlab.com)

mentioned in commit ebea63a3ff

mentioned in commit ebea63a3ff430ab4b1937ba8ac60c06765cd919a
paulhitt (Migrated from gitlab.com) closed this issue 2026-09-07 16:31:06 +00:00
Sign in to join this conversation.