| name | altcha |
| description | Comprehensive guide for building spam/bot protection solutions with the Altcha Rust library. Use this skill whenever the user wants to: implement ALTCHA proof-of-work (PoW) challenges in Rust to protect forms, APIs, or web applications from bots and spam; create cryptographic challenges using PBKDF2, SHA-256/384/512, scrypt, or Argon2id algorithms; solve ALTCHA PoW challenges on the client side; verify challenge solutions on the server side; set up deterministic mode with key signatures for fast-path verification; integrate ALTCHA Sentinel server signature verification; verify form field hashes for tamper detection; build challenge-response endpoints in Rust web frameworks (Axum, Actix-web, Rocket, etc.); configure challenge difficulty via cost, key prefix, and algorithm parameters; handle challenge expiration and HMAC-based payload signing; or work with any ALTCHA protocol feature in Rust. Make sure to use this skill whenever the user mentions altcha, proof-of-work captcha, bot protection, spam protection, PoW challenge, ALTCHA integration, anti-bot Rust, challenge-response authentication, or any ALTCHA-related functionality in Rust. |
Altcha — Rust Proof-of-Work Bot Protection Library
Altcha is the official Rust implementation of the ALTCHA Proof-of-Work (v2) protocol. It provides cryptographic challenge-response verification to protect web applications, forms, and APIs from bots and spam — without relying on CAPTCHAs that degrade user experience. The library is wire-compatible with the JavaScript ALTCHA widget, so a Rust backend can serve challenges that any ALTCHA JS frontend widget can solve.
This skill covers the altcha crate (v0.1.0+, MIT license, crate name: altcha). A separate community crate altcha-lib-rs exists but is less complete — focus on the official altcha crate from the altcha-org GitHub organization.
Crate Architecture
The crate is a single crate with no sub-crates. All public items are re-exported at the crate root:
altcha (crate root)
├── create_challenge() — Generate a new PoW challenge
├── solve_challenge() — Solve a challenge (find the counter)
├── verify_solution() — Verify a client-submitted solution
├── sign_challenge() — Manually sign challenge parameters
├── verify_server_signature() — Verify ALTCHA Sentinel payloads
├── verify_fields_hash() — Verify form field integrity hashes
├── parse_verification_data() — Parse URL-encoded verification data
├── Error / Result — Error types
├── HmacAlgorithm — HMAC algorithm enum (SHA-256/384/512)
├── CreateChallengeOptions — Challenge creation configuration
├── Challenge / ChallengeParameters — Challenge data structures
├── Solution / Payload — Solution and combined payload types
├── SolveChallengeOptions — Solver configuration
├── VerifySolutionOptions — Verification configuration
├── VerifySolutionResult — Verification outcome
├── ServerSignaturePayload — ALTCHA Sentinel payload
├── ServerSignatureVerificationData — Parsed verification data
└── VerifyServerSignatureResult — Server signature verification outcome
Quick Start: Minimal Dependency Setup
Add the crate to your Cargo.toml. PBKDF2 and iterative SHA algorithms are always available without feature flags. Enable optional features for Argon2id and scrypt:
[dependencies]
altcha = "0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rand = "0.9"
[dependencies]
altcha = { version = "0", features = ["argon2", "scrypt"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
base64 = "0.22"
rand = "0.9"
Feature Flags
| Feature | Default | Enables | Algorithm |
|---|
| (none) | -- | PBKDF2/SHA-256, PBKDF2/SHA-384, PBKDF2/SHA-512, SHA-256, SHA-384, SHA-512 | Key derivation via PBKDF2 or iterative hashing |
argon2 | no | Argon2id KDF | Memory-hard algorithm, requires memory_cost |
scrypt | no | scrypt KDF | Memory-hard algorithm, requires memory_cost |
Quick Reference: Which Guide Do I Need?
- Full PoW flow (create → solve → verify) → Read
references/core-pow-flow.md
- Server integration patterns (Axum, Actix, etc.) → Read
references/server-integration.md
- ALTCHA JS widget (HTML, React, Vue, Next.js) → Read
references/frontend-integration.md
- All algorithms, cost tuning, and difficulty → Read
references/algorithms-config.md
- ALTCHA Sentinel server signature verification → Read
references/server-signature.md
- Deterministic mode and key signatures → Read
references/deterministic-mode.md
- Form field hash verification → Read
references/fields-hash.md
- All public types, error handling, serde → Read
references/types-errors.md
Core Patterns at a Glance
1. Basic PoW Flow (Server Creates Challenge, Client Solves, Server Verifies)
This is the fundamental ALTCHA pattern. The server generates a challenge, the client (usually a JS widget) solves it by brute-forcing a counter value, and the server verifies the solution.
use altcha::{
create_challenge, solve_challenge, verify_solution,
CreateChallengeOptions, SolveChallengeOptions, VerifySolutionOptions,
};
fn main() -> altcha::Result<()> {
let challenge = create_challenge(CreateChallengeOptions {
algorithm: "PBKDF2/SHA-256".to_string(),
cost: 5_000,
hmac_signature_secret: Some("my-server-secret".to_string()),
..Default::default()
})?;
let challenge_json = serde_json::to_string(&challenge)?;
println!("Challenge: {}", challenge_json);
let solution = solve_challenge(SolveChallengeOptions::new(&challenge))?
.expect("solution found within timeout");
let result = verify_solution(VerifySolutionOptions::new(
&challenge,
&solution,
"my-server-secret",
))?;
println!("Verified: {}", result.verified);
assert!(result.verified);
Ok(())
}
2. Challenge with Expiration
Set expires_at to a Unix timestamp to make challenges expire after a time window. Expired challenges always fail verification regardless of whether the solution is mathematically correct.
use altcha::*;
fn main() -> altcha::Result<()> {
let expires_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() + 600;
let challenge = create_challenge(CreateChallengeOptions {
algorithm: "PBKDF2/SHA-256".to_string(),
cost: 5_000,
expires_at: Some(expires_at),
hmac_signature_secret: Some("secret".to_string()),
..Default::default()
})?;
let result = verify_solution(VerifySolutionOptions::new(
&challenge, &solution, "secret",
))?;
if result.expired {
println!("Challenge expired — generate a new one");
}
Ok(())
}
3. Using Different Algorithms
Each algorithm has different cost parameters and difficulty characteristics. The cost field means different things depending on the algorithm — see references/algorithms-config.md for details.
use altcha::*;
let sha_challenge = create_challenge(CreateChallengeOptions {
algorithm: "SHA-256".to_string(),
cost: 1_000_000,
..Default::default()
})?;
let pbkdf2_challenge = create_challenge(CreateChallengeOptions {
algorithm: "PBKDF2/SHA-256".to_string(),
cost: 50_000,
..Default::default()
})?;
let scrypt_challenge = create_challenge(CreateChallengeOptions {
algorithm: "SCRYPT".to_string(),
cost: 16384,
memory_cost: Some(8),
..Default::default()
})?;
let argon2_challenge = create_challenge(CreateChallengeOptions {
algorithm: "ARGON2ID".to_string(),
cost: 2,
memory_cost: Some(65536),
parallelism: Some(2),
..Default::default()
})?;
4. Deterministic Mode with Key Signatures
Deterministic mode lets the server skip re-deriving the key during verification, making verification significantly faster. The server pre-computes the expected key prefix from a counter value and signs it with a separate key signature secret.
use altcha::*;
use rand::Rng;
fn main() -> altcha::Result<()> {
let counter = rand::rng().random_range(5_000..=10_000);
let challenge = create_challenge(CreateChallengeOptions {
algorithm: "PBKDF2/SHA-256".to_string(),
cost: 5_000,
counter: Some(counter),
expires_at: Some(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() + 600,
),
hmac_signature_secret: Some("my-hmac-secret".to_string()),
hmac_key_signature_secret: Some("my-key-secret".to_string()),
..Default::default()
})?;
let result = verify_solution(VerifySolutionOptions {
hmac_key_signature_secret: Some("my-key-secret".to_string()),
..VerifySolutionOptions::new(&challenge, &solution, "my-hmac-secret")
})?;
assert!(result.verified);
Ok(())
}
5. ALTCHA Sentinel Server Signature Verification
ALTCHA Sentinel is a server-side service that analyzes submissions for spam/bot patterns. It returns a signed payload that your Rust backend can verify cryptographically.
use altcha::{verify_server_signature, ServerSignaturePayload, parse_verification_data};
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
fn handle_sentinel_payload(encoded_payload: &str) -> altcha::Result<()> {
let decoded = BASE64.decode(encoded_payload)
.map_err(|e| altcha::Error::InvalidParameters(format!("base64 decode: {}", e)))?;
let payload: ServerSignaturePayload = serde_json::from_slice(&decoded)?;
let result = verify_server_signature(&payload, "my-hmac-secret")?;
if result.verified {
if let Some(data) = &result.verification_data {
println!("Classification: {:?}", data.classification);
println!("Score: {:?}", data.score);
println!("Reasons: {:?}", data.reasons);
println!("Email: {:?}", data.email);
if data.classification.as_deref() == Some("BAD") {
println!("Bot detected — reject submission");
}
}
}
Ok(())
}
6. Fields Hash Verification
Protect form fields from client-side tampering. The server generates an HMAC hash of selected field values. On form submission, the server re-computes the hash and compares — if the client modified any field, the hash won't match.
use altcha::{verify_fields_hash, HmacAlgorithm};
use std::collections::HashMap;
fn verify_form_integrity() -> bool {
let mut form_data = HashMap::new();
form_data.insert("email".to_string(), "user@example.com".to_string());
form_data.insert("name".to_string(), "John Doe".to_string());
form_data.insert("message".to_string(), "Hello world!".to_string());
let fields = vec!["email".to_string(), "name".to_string()];
let expected_hash = "a1b2c3d4...";
verify_fields_hash(&form_data, &fields, expected_hash, Some(&HmacAlgorithm::Sha256))
}
Key Concepts
How ALTCHA Proof-of-Work Works
ALTCHA is a challenge-response protocol designed to make automated submissions expensive without requiring users to solve puzzles:
- Server generates a challenge: a random
salt + nonce, an algorithm, a cost (difficulty), and a key_prefix (e.g., "00" meaning the derived key must start with "00").
- Client receives the challenge and brute-forces a
counter value. For each counter, it derives a key using the specified algorithm. When the derived key starts with the required prefix, it submits the counter and derived_key back.
- Server verifies by either re-deriving the key with the submitted counter (standard mode) or checking the HMAC key signature (deterministic mode). This confirms the client actually performed the work.
The difficulty is controlled by the key_prefix and cost parameters. A longer prefix (e.g., "0000") exponentially increases the expected number of iterations.
Supported Algorithm Strings
| Algorithm String | KDF | Feature Flag | Cost Meaning |
|---|
"PBKDF2/SHA-256" | PBKDF2-HMAC-SHA-256 | — (always) | Number of iterations |
"PBKDF2/SHA-384" | PBKDF2-HMAC-SHA-384 | — | Number of iterations |
"PBKDF2/SHA-512" | PBKDF2-HMAC-SHA-512 | — | Number of iterations |
"SHA-256" | Iterative SHA-256 | — | Number of hash iterations |
"SHA-384" | Iterative SHA-384 | — | Number of hash iterations |
"SHA-512" | Iterative SHA-512 | — | Number of hash iterations |
"SCRYPT" | scrypt | scrypt | N (CPU/memory cost, power of 2) |
"ARGON2ID" | Argon2id | argon2 | Time cost (iterations) |
Serialization & Wire Compatibility
All challenge, solution, and payload types implement Serialize/Deserialize with camelCase JSON field names matching the JavaScript ALTCHA library exactly. This means you can serve challenges from a Rust backend and verify them with the official ALTCHA JS widget on the frontend — they are fully wire-compatible.
Key field name mappings: key_length → "keyLength", key_prefix → "keyPrefix", expires_at → "expiresAt", derived_key → "derivedKey", verification_data → "verificationData".
Security Properties
- Constant-time comparison: All HMAC and hash comparisons use the
subtle crate to prevent timing attacks.
- HMAC signing: Challenge payloads can be HMAC-signed so the server can detect tampering. An unsigned challenge is insecure because a client could modify the parameters.
- Expiration: Challenges support
expires_at timestamps. Verification always checks expiration first — expired challenges are rejected before any cryptographic verification.
- No async required: All functions are synchronous and CPU-bound. In async web frameworks, wrap solver calls in
tokio::task::spawn_blocking to avoid blocking the async runtime.
Default Values for CreateChallengeOptions
When using Default::default() or ..Default::default():
algorithm: "PBKDF2/SHA-256"
cost: 100_000 (100K PBKDF2 iterations)
key_length: 32 bytes
key_prefix: "00" (derived key must start with "00")
hmac_algorithm: HmacAlgorithm::Sha256
- All other fields:
None or empty
Common Pitfalls
-
Forgetting to sign challenges — Always set hmac_signature_secret when creating challenges. Without signing, a malicious client could modify challenge parameters (e.g., reduce cost to 1) and submit a trivial solution.
-
Mismatched secrets — The hmac_signature_secret used in create_challenge() must be the same string passed to verify_solution(). The hmac_key_signature_secret must also match between creation and verification if using deterministic mode.
-
Blocking the async runtime — solve_challenge() is CPU-bound and can take seconds. In async Rust (Axum, Actix, Tokio), always wrap it: let solution = tokio::task::spawn_blocking(move || solve_challenge(opts)).await.expect("spawn_blocking failed")?;
-
Scrypt cost must be a power of 2 — The cost parameter for scrypt is the N parameter and must be a power of 2 (e.g., 1024, 4096, 16384, 32768). Other values will cause an error.
-
Argon2id requires memory_cost — Unlike PBKDF2/SHA where memory_cost is optional, Argon2id mandates memory_cost (in KiB). Recommended minimum is 65536 (64 MB).
-
Key prefix length — The default key_prefix of "00" means approximately 1 in 256 attempts will succeed on average. Use "000" for ~1/4096 or "0000" for ~1/65536 difficulty. Longer prefixes mean longer solve times for legitimate users.
Reference Files
For detailed information on any topic, read the appropriate reference file:
references/core-pow-flow.md — Complete PoW lifecycle: create_challenge, solve_challenge, verify_solution, Payload struct, solver timeout and counter stepping
references/server-integration.md — Axum endpoint examples (GET /challenge, POST /verify), payload type detection (PoW vs Sentinel), Actix-web patterns, error responses
references/frontend-integration.md - ALTCHA JS widget (HTML, React, Vue, Next.js)
references/algorithms-config.md — Deep dive into all 8 algorithms, cost parameter tuning, difficulty calibration, memory_cost and parallelism settings, performance benchmarks
references/server-signature.md — ALTCHA Sentinel integration: ServerSignaturePayload, verify_server_signature, parse_verification_data, verification data fields (classification, score, reasons), base64 decoding
references/deterministic-mode.md — Deterministic challenges with counter and hmac_key_signature_secret, fast-path verification, sign_challenge function, when to use deterministic vs standard mode
references/fields-hash.md — verify_fields_hash usage, protecting form fields from tampering, algorithm options (SHA-256/384/512), HashMap form data patterns
references/types-errors.md — Complete reference for all public structs, enums, Error variants, HmacAlgorithm, serde configuration, type aliases, constructor methods (SolveChallengeOptions::new, VerifySolutionOptions::new)