Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Hashcash Captcha

The Hashcash Captcha provides proof-of-work based challenge-response authentication to prevent automated attacks. Unlike traditional image-based CAPTCHAs, it requires clients to perform computational work, making it accessible and bot-resistant.

Purpose

Hashcash CAPTCHA protects sensitive operations from automated abuse by requiring clients to solve a cryptographic puzzle. The difficulty can be tuned to balance security and user experience - higher difficulty means more computation time required.

How It Works

Proof-of-Work Challenge

The system uses a challenge-response protocol:

  1. Client requests a challenge - Specifies desired difficulty (19-35) and time-to-live
  2. Server generates a random 32-byte challenge - Stores it in Redis with the specified TTL
  3. Client solves the puzzle - Finds a nonce value that when hashed with the challenge produces a hash with the required number of leading zero bits
  4. Client submits the solution - Sends back the challenge ID and nonce
  5. Server verifies - Checks the solution and deletes the challenge if valid (one-time use)

The difficulty parameter controls how many leading zero bits are required in the SHA256 hash output. Each increment doubles the expected computation time.

Challenge Lifecycle

Challenges are stateful and stored in Redis:

  • Each challenge has a unique 16-byte ID
  • TTL ensures challenges expire and don’t accumulate
  • Successfully verified challenges are deleted immediately (prevents replay attacks)
  • Expired or non-existent challenges return NotFound result

Frontend Integration

Basic Workflow

1. Before submitting sensitive operation (login, registration, etc.)
2. Call RequestCaptcha with difficulty and TTL
3. Receive challenge_id, challenge bytes, and difficulty
4. Solve: Find nonce where SHA256(challenge + nonce) has required leading zero bits
5. Call VerifyCaptcha with challenge_id and nonce
6. Check result: Pass/Fail/NotFound
7. If Pass, proceed with the protected operation

Solving the Challenge

Frontend must implement the proof-of-work algorithm:

  • Hash function: SHA256
  • Input: challenge_bytes + nonce_bytes (nonce as 8-byte big-endian u64)
  • Goal: Find nonce where hash has difficulty leading zero bits
  • Method: Brute force increment nonce from 0 until valid hash found

JavaScript/TypeScript developers should use the Web Crypto API or a library like crypto-js for SHA256 hashing. For performance-critical cases, consider using WebAssembly.

Difficulty Guidelines

Difficulty 19-22 (Very Easy):

  • Solves in <100ms on modern devices
  • Use for non-critical operations or mobile clients

Difficulty 23-26 (Easy to Medium):

  • Solves in 100ms - 1 second
  • Good balance for most use cases (login, registration)

Difficulty 27-30 (Medium to Hard):

  • Solves in 1-10 seconds
  • Use for high-value operations (password reset, account deletion)

Difficulty 31-35 (Very Hard):

  • Solves in 10+ seconds
  • Use sparingly, mainly for administrative or highly sensitive operations

Note: Difficulty is exponential - each +1 doubles the average solve time.

TTL Recommendations

Set TTL based on expected solve time plus user think time:

  • Easy challenges: 30-60 seconds
  • Medium challenges: 60-120 seconds
  • Hard challenges: 120-300 seconds

Too short: Users may timeout while solving Too long: Increases Redis memory usage and replay attack window

Verification Results

Three possible outcomes from VerifyCaptcha:

  • Pass: Solution is correct, challenge consumed and deleted
  • Fail: Solution is incorrect, challenge remains valid (can retry)
  • NotFound: Challenge expired, already used, or never existed

Frontend should handle NotFound by requesting a new challenge, and Fail by continuing to solve or requesting a new one.

Architecture

Redis-Backed Storage

Challenges are stored in Redis with keys: hashcash:{hex(challenge_id)}

Each challenge contains:

  • 16-byte unique ID (random)
  • 32-byte random challenge data
  • Difficulty value (19-35)
  • TTL expiration

Redis automatically cleans up expired challenges, and successful verifications delete them immediately to prevent replay attacks.

Stateless and Distributed

The system is stateless from the application perspective - all state lives in Redis. This allows multiple server instances to handle challenge requests and verifications without coordination.

When to Use Hashcash

Good use cases:

  • Registration and login endpoints
  • Password reset requests
  • Anonymous or pre-authentication operations
  • Expensive public endpoints (email sending, report generation)
  • Rate limiting supplement for unauthenticated users

When NOT to use:

  • Authenticated user operations (use rate limiting instead)
  • High-frequency read operations
  • Mobile-first applications (difficulty must be tuned lower)
  • Accessibility-critical features (consider alternatives)

Implementation Notes

  • Uses the Processor pattern for challenge creation and verification
  • Challenge IDs must be exactly 16 bytes
  • Difficulty validation: must be in range [19, 35] inclusive
  • TTL validation: must be at least 1 second
  • One challenge = one verification (single-use)
  • Verification is atomic: check and delete happen together
  • All cryptographic operations use SHA256