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:
# Cargo.toml
# Basic: PBKDF2 + SHA only (sufficient for most use cases)
[dependencies]
altcha = "0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rand = "0.9"
# Full: all algorithms including Argon2id and scrypt, plus base64 for Sentinel
[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<()> {
// --- SERVER SIDE: Create a signed challenge ---
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()
})?;
// The challenge JSON is sent to the client (usually via an API endpoint)
let challenge_json = serde_json::to_string(&challenge)?;
println!("Challenge: {}", challenge_json);
// --- CLIENT SIDE: Solve the challenge ---
// The client iterates counters until the derived key starts with the prefix
let solution = solve_challenge(SolveChallengeOptions::new(&challenge))?
.expect("solution found within timeout");
// --- SERVER SIDE: Verify the submitted solution ---
let result = verify_solution(VerifySolutionOptions::new(
&challenge,
&solution,
"my-server-secret", // must match the secret used to create the challenge
))?;
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; // 10 minutes from now
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()
})?;
// ... solve and verify as before ...
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::*;
// SHA-256 iterative hashing — lighter CPU work, simpler
let sha_challenge = create_challenge(CreateChallengeOptions {
algorithm: "SHA-256".to_string(),
cost: 1_000_000, // number of hash iterations
..Default::default()
})?;
// PBKDF2/SHA-256 — the default, good balance of security and speed
let pbkdf2_challenge = create_challenge(CreateChallengeOptions {
algorithm: "PBKDF2/SHA-256".to_string(),
cost: 50_000, // number of PBKDF2 iterations
..Default::default()
})?;
// SCRYPT — memory-hard, much harder to optimize with GPUs/ASICs
// Requires feature = "scrypt" in Cargo.toml
let scrypt_challenge = create_challenge(CreateChallengeOptions {
algorithm: "SCRYPT".to_string(),
cost: 16384, // N parameter (must be power of 2)
memory_cost: Some(8), // r parameter (block size)
..Default::default()
})?;
// ARGON2ID — most modern memory-hard algorithm
// Requires feature = "argon2" in Cargo.toml
let argon2_challenge = create_challenge(CreateChallengeOptions {
algorithm: "ARGON2ID".to_string(),
cost: 2, // time cost (iterations)
memory_cost: Some(65536), // memory in KiB (64 MB)
parallelism: Some(2), // parallelism factor
..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);
// Create challenge with both secrets
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()
})?;
// ... client solves the challenge ...
// Verify with the key signature secret for fast-path verification
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<()> {
// Decode the base64 payload from the hidden form field
let decoded = BASE64.decode(encoded_payload)
.map_err(|e| altcha::Error::InvalidParameters(format!("base64 decode: {}", e)))?;
let payload: ServerSignaturePayload = serde_json::from_slice(&decoded)?;
// Verify the server signature
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);
// Check if the submission should be rejected
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..."; // HMAC hash provided by the client (originally computed by server)
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, analgorithm, acost(difficulty), and akey_prefix(e.g.,"00"meaning the derived key must start with "00"). - Client receives the challenge and brute-forces a
countervalue. For each counter, it derives a key using the specified algorithm. When the derived key starts with the required prefix, it submits thecounterandderived_keyback. - 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
subtlecrate 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_attimestamps. 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_blockingto 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:32byteskey_prefix:"00"(derived key must start with "00")hmac_algorithm:HmacAlgorithm::Sha256- All other fields:
Noneor empty
Common Pitfalls
-
Forgetting to sign challenges — Always set
hmac_signature_secretwhen creating challenges. Without signing, a malicious client could modify challenge parameters (e.g., reducecostto 1) and submit a trivial solution. -
Mismatched secrets — The
hmac_signature_secretused increate_challenge()must be the same string passed toverify_solution(). Thehmac_key_signature_secretmust 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
costparameter 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_prefixof"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 steppingreferences/server-integration.md— Axum endpoint examples (GET /challenge, POST /verify), payload type detection (PoW vs Sentinel), Actix-web patterns, error responsesreferences/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 benchmarksreferences/server-signature.md— ALTCHA Sentinel integration: ServerSignaturePayload, verify_server_signature, parse_verification_data, verification data fields (classification, score, reasons), base64 decodingreferences/deterministic-mode.md— Deterministic challenges with counter and hmac_key_signature_secret, fast-path verification, sign_challenge function, when to use deterministic vs standard modereferences/fields-hash.md— verify_fields_hash usage, protecting form fields from tampering, algorithm options (SHA-256/384/512), HashMap form data patternsreferences/types-errors.md— Complete reference for all public structs, enums, Error variants, HmacAlgorithm, serde configuration, type aliases, constructor methods (SolveChallengeOptions::new, VerifySolutionOptions::new)