
Altcha
- 10 installs
- Updated May 4, 2026
- melonask/altcha-skills
Helps with ai & agent building tasks.
About
altcha is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- altcha
- AI & Agent Building
- AI-coding skill
Altcha by the numbers
- 10 all-time installs (skills.sh)
- Ranked #11,937 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/melonask/altcha-skills --skill altchaAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 10 |
|---|---|
| Last updated | May 4, 2026 |
| Repository | melonask/altcha-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
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 outcomeQuick 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:
1. 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"). 2. 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. 3. 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
1. 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.
2. 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.
3. 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")?;
4. 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.
5. Argon2id requires memory_cost — Unlike PBKDF2/SHA where memory_cost is optional, Argon2id mandates memory_cost (in KiB). Recommended minimum is 65536 (64 MB).
6. 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 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)
Known Issues in SKILL.md (as of April 2026)
This document records verified discrepancies between the altcha-skills documentation and the actual altcha crate v0.1.0 API. Each entry includes the compiler error produced by a real-world test project and the fix that makes it compile and run.
---
Issue 1: verify_fields_hash complete signature mismatch (UPDATED)
Skill claim: verify_fields_hash(algorithm: &HmacAlgorithm, secret: &str, fields: &[(&str, &str)], hash: &str) or verify_fields_hash(algorithm: Option<&str>, ...) Actual API (v0.1.0): verify_fields_hash(form_data: &HashMap<String, String>, fields: &[String], fields_hash: &str, algorithm: Option<&HmacAlgorithm>) -> bool
The skill describes this function with completely different parameter ordering and types. The actual function:
- Takes a
HashMap<String, String>of all form data as the first argument - Takes a
Vec<String>(or&[String]) of field names as the second argument - Takes a hash string as the third argument
- Takes an optional
HmacAlgorithmas the last argument - Returns
bool(notResult)
Affected locations
SKILL.mdline 281:verify_fields_hash(..., Some("SHA-256"))references/fields-hash.mdlines 67, 107, 124, 139, 159, 192, 209references/types-errors.mdline 321
Fix
use altcha::HmacAlgorithm;
use std::collections::HashMap;
let form_data: HashMap<String, String> = HashMap::new();
let fields: Vec<String> = vec!["field1".to_string()];
let result = verify_fields_hash(&form_data, &fields, "expected-hash", Some(&HmacAlgorithm::Sha256));---
Issue 2: duration_since(std::time::UNIX_EPOCH)? inside altcha::Result
Skill claim: Multiple examples use ? on std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) inside functions returning altcha::Result<()>. Actual API: altcha::Error does not implement From<std::time::SystemTimeError>.
Affected locations
SKILL.mdlines 126, 209references/core-pow-flow.mdline 18references/deterministic-mode.mdline 51
Compiler error
error[E0277]: `?` couldn't convert the error to `altcha::Error`
--> src/main.rs:126:55
|
126 | .duration_since(std::time::UNIX_EPOCH)?
| -------------------------------------^ the trait `From<SystemTimeError>` is not implemented for `altcha::Error`Fix
Use .unwrap() or .unwrap_or_default() (duration errors are infrequent for wall-clock time), or map the error:
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();Or map the error explicitly:
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|e| altcha::Error::InvalidParameters(format!("time error: {}", e)))?
.as_secs();---
Issue 3: serde_json and serde are required dependencies but not listed in Quick Start
Skill claim: Quick Start shows altcha = "0" as the only dependency, but many examples use serde_json::to_string and the server-integration pattern requires serde with #[derive(Deserialize)]. Actual behavior: serde_json and serde are not re-exported by altcha. A project following only the Quick Start Cargo.toml will fail to compile when copying the examples.
Affected locations
SKILL.mdlines 47-51 (Quick Start Cargo.toml)SKILL.mdline 95 (serde_json::to_string(&challenge)?)references/server-integration.mdlines 11-20 (Cargo.toml listing serde/serde_json)
Compiler error
error[E0433]: cannot find module or crate `serde_json` in this scopeFix
Add explicit dependencies to the Quick Start snippet:
[dependencies]
altcha = { version = "0", features = ["argon2", "scrypt"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
base64 = "0.22"Note: base64 is also required for the Sentinel/server-signature examples.
---
Issue 4: ChallengeParameters does not implement Default
Skill claim: Multiple examples use ..Default::default() when constructing ChallengeParameters manually. Actual API: ChallengeParameters does not derive Default.
Affected locations
references/core-pow-flow.mdline 243 (was..Default::default()— already fixed in code example but the structural description omits it)- ~~
references/deterministic-mode.mdline 140~~ — The example already fills all fields explicitly, so this was a false positive.
Compiler error
error[E0277]: the trait bound `ChallengeParameters: Default` is not satisfied
--> src/main.rs:187:11
|
187 | ..Default::default()
| ^^^^^^^^^^^^^^^^^^ the trait `Default` is not implemented for `ChallengeParameters`Fix
Explicitly fill every field of ChallengeParameters:
let mut params = ChallengeParameters {
algorithm: "PBKDF2/SHA-256".to_string(),
cost: 5_000,
nonce: "random-nonce".to_string(),
salt: "random-salt".to_string(),
key_prefix: "00".to_string(),
key_length: 32,
data: None,
expires_at: None,
key_signature: None,
memory_cost: None,
parallelism: None,
};---
Issue 5: BASE64.decode(...)? inside altcha::Result
Skill claim: The handle_sentinel_payload example uses BASE64.decode(encoded_payload)? inside a function returning altcha::Result<()>. Actual API: altcha::Error does not have a From<base64::DecodeError> impl, so the ? operator fails.
Affected locations
SKILL.mdlines 239-245references/server-signature.mdlines 65-74
Compiler error
error[E0277]: the `?` operator can only be used in a function that returns `Result`\
... base64::DecodeError is not convertible to altcha::ErrorFix
Handle the base64 decode error separately, e.g.:
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")?;
// ...
Ok(())
}---
Issue 6: rand::thread_rng() deprecated in rand 0.9+
Skill claim: Uses rand::thread_rng().gen_range(...). Actual behavior: Deprecated in rand 0.9 (April 2026). rand::rng() and random_range are the replacements.
Affected locations
SKILL.mddeterministic mode example (usesrand::rng().random_range(...)in current skill, so this is now correct)references/server-integration.mdCargo.toml now requiresrand = "0.9"
Note: rand is not a transitive dependency of altcha (altcha depends on rand 0.8 internally but does not re-export it). Users must add rand to their own Cargo.toml.
Compiler warning (if using rand 0.9 without matching API or rand 0.8 on rand::rng())
error[E0433]: cannot find module or crate `rand` in this scopeOr with rand 0.8:
error[E0599]: no method named `random_range` found for struct `ThreadRng`Fix
Use rand = "0.9" in your own Cargo.toml and write:
use rand::Rng;
let counter = rand::rng().random_range(5_000..=10_000);If you prefer rand 0.8, use rand::thread_rng().gen_range(5_000..=10_000) instead.
---
---
Issue 7: spawn_blocking cannot use ? inside altcha::Result
Skill claim: The async example uses spawn_blocking(move || { solve_challenge(...) }).await?? inside a function returning altcha::Result<()>. Actual API: tokio::task::spawn_blocking returns Result<Result<_, altcha::Error>, JoinError>. The JoinError from spawn_blocking itself cannot be converted to altcha::Error via ?.
Affected locations
references/core-pow-flow.mdline 222
Fix
// WRONG:
let solution = tokio::task::spawn_blocking(move || {
solve_challenge(SolveChallengeOptions::new(&challenge_clone))
}).await??;
// CORRECT: Handle JoinError explicitly
let solution = tokio::task::spawn_blocking(move || {
solve_challenge(SolveChallengeOptions::new(&challenge_clone))
}).await.expect("spawn_blocking failed")?;Test Projects Used for Verification
All issues above were discovered by compiling multiple real-world test binaries against altcha = "0.1" (crates.io, April 2026). The test projects are in this repo:
test_project/— corrected basic test (includes all SKILL.md examples)test_axum/— corrected Axum server integration exampletest_all_references/— comprehensive reference test covering every algorithm and API surface
Build any test project with:
cd test_all_references
cargo run --releasealtcha-skills
A comprehensive skill for building spam/bot protection solutions using the Altcha Rust crate — the official Rust implementation of the ALTCHA Proof-of-Work (v2) protocol. Enables the LLM to accurately create, solve, and verify cryptographic PoW challenges, integrate ALTCHA into Rust web frameworks, and configure difficulty parameters for production deployments.
Overview
The ALTCHA protocol protects web applications from bots and spam using cryptographic proof-of-work challenges — no CAPTCHAs required. This skill provides everything needed to build complete ALTCHA solutions.
Installation
npx skills add melonask/altcha-skillsWhat This Skill Covers
| Capability | Description |
|---|---|
| Challenge creation | Generate signed PoW challenges with configurable algorithms and difficulty |
| Challenge solving | Brute-force counter values to find matching key prefixes |
| Solution verification | Three-step verification: expiration, signature, solution correctness |
| 8 KDF algorithms | PBKDF2 (SHA-256/384/512), SHA (256/384/512), scrypt, Argon2id |
| Deterministic mode | Pre-signed key prefixes for O(1) fast-path verification |
| ALTCHA Sentinel | Verify server-side spam analysis signatures |
| Fields hash | Detect client-side form field tampering via HMAC hashes |
| Web framework integration | Complete Axum server, Actix-web patterns, payload type detection |
| Frontend widget | ALTCHA JS widget setup for React, Vue, Next.js, vanilla HTML |
| Error handling | All error variants, serde wire-format compatibility |
File Structure
altcha/
├── SKILL.md # Main skill — overview, quick start, core patterns
└── references/
├── core-pow-flow.md # Create → Solve → Verify lifecycle
├── server-integration.md # Axum & Actix-web endpoint examples
├── frontend-integration.md # ALTCHA JS widget (HTML, React, Vue, Next.js)
├── algorithms-config.md # All 8 algorithms, cost tuning, difficulty tables
├── server-signature.md # ALTCHA Sentinel verification
├── deterministic-mode.md # Fast-path verification with key signatures
├── fields-hash.md # Form field integrity protection
└── types-errors.md # Complete API reference (all structs, enums, errors)Quick Start
Cargo.toml
# Basic (PBKDF2 + SHA only)
[dependencies]
altcha = "0"
# All algorithms
[dependencies]
altcha = { version = "0", features = ["argon2", "scrypt"] }Minimal Example
use altcha::{create_challenge, solve_challenge, verify_solution, CreateChallengeOptions};
fn main() -> altcha::Result<()> {
// Server: create a signed challenge
let challenge = create_challenge(CreateChallengeOptions {
algorithm: "PBKDF2/SHA-256".to_string(),
cost: 5_000,
hmac_signature_secret: Some("secret".to_string()),
..Default::default()
})?;
// Client: solve it
let solution = solve_challenge(SolveChallengeOptions::new(&challenge))?
.expect("solution found");
// Server: verify
let result = verify_solution(VerifySolutionOptions::new(&challenge, &solution, "secret"))?;
assert!(result.verified);
Ok(())
}Reference Guide
The skill uses progressive disclosure. Start with SKILL.md, then read the appropriate reference:
| You want to... | Read |
|---|---|
| Understand the full PoW lifecycle | references/core-pow-flow.md |
| Build Axum/Actix endpoints | references/server-integration.md |
| Set up the JS widget on the frontend | references/frontend-integration.md |
| Choose an algorithm and tune difficulty | references/algorithms-config.md |
| Integrate ALTCHA Sentinel | references/server-signature.md |
| Speed up verification with deterministic mode | references/deterministic-mode.md |
| Protect form fields from tampering | references/fields-hash.md |
| Look up a type, function, or error | references/types-errors.md |
Trigger Keywords
This skill activates when the user mentions:
- Library name: altcha, altcha-rs, altcha-lib-rs
- Concepts: proof-of-work captcha, bot protection, spam protection, PoW challenge, challenge-response authentication
- Actions: create ALTCHA challenge, verify ALTCHA solution, solve PoW challenge
- Integration: ALTCHA Axum, ALTCHA Actix, ALTCHA widget, ALTCHA Sentinel
- Algorithms: PBKDF2 challenge, SHA PoW, scrypt challenge, Argon2id PoW
Supported Algorithms
| Algorithm | Feature Flag | Best For |
|---|---|---|
| PBKDF2/SHA-256 | (default) | General use, balanced |
| PBKDF2/SHA-384 | (default) | Higher security margin |
| PBKDF2/SHA-512 | (default) | Maximum PBKDF2 security |
| SHA-256 | (default) | Fast iterations, high-traffic sites |
| SHA-384 | (default) | Larger hash output |
| SHA-512 | (default) | Largest hash output |
| SCRYPT | scrypt | Memory-hard, GPU-resistant |
| ARGON2ID | argon2 | Most modern, memory-hard |
Requirements
serde/serde_json— JSON serialization (camelCase for JS compatibility)hmac/sha2/digest— HMAC signing and hash operationspbkdf2— PBKDF2 key derivation (always available)argon2(optional) — Argon2id KDFscrypt(optional) — scrypt KDFrand— Random nonce/salt generationhex— Hex encoding/decodingsubtle— Constant-time comparison (prevents timing attacks)thiserror— Error derive macro
Wire Compatibility
The Rust crate produces JSON with camelCase field names matching the official JavaScript ALTCHA library. This means a Rust backend can serve challenges that any ALTCHA JS frontend widget can solve — they are fully interoperable.
License
This skill references the altcha Rust crate, which is licensed under the MIT License.
Algorithms & Configuration — Difficulty Tuning Guide
This reference covers all supported algorithms, their cost parameters, and how to tune challenge difficulty for your use case.
Algorithm Overview
The ALTCHA Rust crate supports 8 algorithm strings across 4 key derivation function families:
| Algorithm String | KDF Family | Feature Flag | Security Level |
|---|---|---|---|
"PBKDF2/SHA-256" | PBKDF2 | — (always) | Good — standard, widely analyzed |
"PBKDF2/SHA-384" | PBKDF2 | — | Higher — larger hash output |
"PBKDF2/SHA-512" | PBKDF2 | — | Higher — largest hash output |
"SHA-256" | Iterative SHA | — | Lighter — simpler than PBKDF2 |
"SHA-384" | Iterative SHA | — | Lighter — larger hash output |
"SHA-512" | Iterative SHA | — | Lighter — largest hash output |
"SCRYPT" | scrypt | scrypt | High — memory-hard, GPU-resistant |
"ARGON2ID" | Argon2id | argon2 | Highest — winner of PHC, memory-hard |
Cost Parameter Meanings
The cost field in CreateChallengeOptions has different meanings depending on the algorithm. This is critical to understand for correct configuration.
PBKDF2 Algorithms
For "PBKDF2/SHA-256", "PBKDF2/SHA-384", "PBKDF2/SHA-512":
- `cost` = Number of PBKDF2 iterations (integer)
- Typical values:
10_000to100_000for moderate difficulty - Higher values = slower solving but also slower verification on the server
- Each iteration performs one HMAC computation
// Low difficulty — fast to solve (~50ms)
CreateChallengeOptions {
algorithm: "PBKDF2/SHA-256".into(),
cost: 1_000,
..Default::default()
}
// Medium difficulty — balanced (~500ms)
CreateChallengeOptions {
algorithm: "PBKDF2/SHA-256".into(),
cost: 50_000,
..Default::default()
}
// High difficulty — slow to solve (~2-5s)
CreateChallengeOptions {
algorithm: "PBKDF2/SHA-256".into(),
cost: 100_000,
..Default::default()
}Iterative SHA Algorithms
For "SHA-256", "SHA-384", "SHA-512":
- `cost` = Number of hash iterations (integer)
- These are simpler than PBKDF2 (no HMAC wrapping) — slightly faster per iteration
- Typical values:
100_000to1_000_000
CreateChallengeOptions {
algorithm: "SHA-256".into(),
cost: 500_000,
..Default::default()
}Scrypt
For "SCRYPT" — requires features = ["scrypt"]:
- `cost` = N parameter — CPU/memory cost factor (must be a power of 2)
- `memory_cost` = r parameter — block size (default: 8)
- `parallelism` = p parameter — parallelization factor (default: 1)
- Memory usage = approximately
128 * N * rbytes
| N (cost) | r (memory_cost) | p (parallelism) | Memory | Difficulty |
|---|---|---|---|---|
| 1024 | 8 | 1 | 1 MB | Low |
| 4096 | 8 | 1 | 4 MB | Medium |
| 16384 | 8 | 1 | 16 MB | High |
| 32768 | 8 | 1 | 32 MB | Very High |
| 65536 | 8 | 1 | 64 MB | Extreme |
CreateChallengeOptions {
algorithm: "SCRYPT".into(),
cost: 16384, // N = 16384 (power of 2)
memory_cost: Some(8), // r = 8
parallelism: Some(1), // p = 1
..Default::default()
}Argon2id
For "ARGON2ID" — requires features = ["argon2"]:
- `cost` = t parameter — time cost / number of iterations
- `memory_cost` = m parameter — memory in KiB (required)
- `parallelism` = p parameter — degree of parallelism (default: 1)
| t (cost) | m (memory_cost) | p (parallelism) | Memory | Difficulty |
|---|---|---|---|---|
| 1 | 32768 (32 MB) | 1 | 32 MB | Low |
| 2 | 65536 (64 MB) | 2 | 64 MB | Medium |
| 3 | 131072 (128 MB) | 2 | 128 MB | High |
| 4 | 262144 (256 MB) | 4 | 256 MB | Very High |
CreateChallengeOptions {
algorithm: "ARGON2ID".into(),
cost: 2, // t = 2 iterations
memory_cost: Some(65536), // m = 64 MB
parallelism: Some(2), // p = 2 threads
..Default::default()
}Difficulty Tuning: Key Prefix
The key_prefix controls the probability that any single counter value will yield a valid solution. Combined with cost, it determines the total expected work:
| Key Prefix | Probability per attempt | Expected attempts |
|---|---|---|
"0" | 1/16 | 16 |
"00" (default) | 1/256 | 256 |
"000" | 1/4096 | 4,096 |
"0000" | 1/65536 | 65,536 |
"00000" | 1/1,048,576 | 1,048,576 |
The total expected solve time is approximately:
expected_time = expected_attempts * time_per_derivationFor example, with PBKDF2/SHA-256 at cost=50,000 and key_prefix="00":
- Each derivation takes roughly 50ms on a modern CPU
- Expected attempts: 256
- Expected total time: ~12.8 seconds (too long!)
With cost=1,000 and key_prefix="00":
- Each derivation takes roughly 1ms
- Expected attempts: 256
- Expected total time: ~256ms (good for most forms)
Recommended Difficulty Settings
| Use Case | Algorithm | Cost | Key Prefix | Expected Time |
|---|---|---|---|---|
| Login form (user patience is low) | PBKDF2/SHA-256 | 1,000 | "00" | ~250ms |
| Contact form | PBKDF2/SHA-256 | 5,000 | "00" | ~1.3s |
| Registration form | PBKDF2/SHA-256 | 10,000 | "00" | ~2.5s |
| High-value action (password reset) | PBKDF2/SHA-256 | 10,000 | "000" | ~41s |
| API rate limiting | SHA-256 | 100,000 | "00" | ~1s |
| Anti-bot (aggressive) | SCRYPT | 16384 | "00" | ~4s |
| Maximum security | ARGON2ID | 2 | "00" | ~5-10s |
Choosing the Right Algorithm
Use this decision tree:
1. Standard web forms → PBKDF2/SHA-256 is the default and well-tested. Good balance of speed and security.
2. High-traffic sites → SHA-256 (iterative) is slightly faster per iteration than PBKDF2. Use when you need to keep server verification time low.
3. Bot-heavy environments → SCRYPT or ARGON2ID are memory-hard. They are much harder to optimize with GPUs, FPGAs, or ASICs, making bot farms expensive to operate.
4. Maximum security → ARGON2ID with high memory cost. This is the most resistant algorithm against specialized hardware attacks.
5. Low-powered clients → Avoid SCRYPT and ARGON2ID on mobile or embedded devices. Stick with SHA-256 or PBKDF2 with lower cost.
HMAC Algorithm Selection
The hmac_algorithm field controls which HMAC variant is used for signing challenges:
use altcha::HmacAlgorithm;
HmacAlgorithm::Sha256 // default, 32-byte output — sufficient for most uses
HmacAlgorithm::Sha384 // 48-byte output — extra security margin
HmacAlgorithm::Sha512 // 64-byte output — maximum HMAC securityThe HMAC algorithm is separate from the challenge KDF algorithm. It only affects the signature verification, not the PoW difficulty. Sha256 is the standard choice.
Production Configuration Checklist
- Always set
hmac_signature_secret— unsigned challenges can be tampered with - Set
expires_atto prevent replay attacks (recommended: 5-15 minutes) - Test solve times on your target client devices (mobile can be 10-50x slower than desktop)
- Monitor solve times in production and adjust cost/prefix accordingly
- For memory-hard algorithms, ensure your server has enough RAM for concurrent verifications
- Store secrets in environment variables, never in code
Core PoW Flow — Create, Solve, Verify
This reference covers the complete proof-of-work lifecycle in the ALTCHA protocol.
1. Creating a Challenge
Use create_challenge() to generate a cryptographic challenge. The function creates a random 16-byte nonce and salt, then optionally signs the challenge with HMAC if hmac_signature_secret is provided.
use altcha::{create_challenge, CreateChallengeOptions};
fn main() -> altcha::Result<()> {
let challenge = create_challenge(CreateChallengeOptions {
algorithm: "PBKDF2/SHA-256".to_string(),
cost: 50_000,
expires_at: Some(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
+ 600,
),
hmac_signature_secret: Some("server-secret-key".to_string()),
key_prefix: "00".to_string(),
key_length: 32,
..Default::default()
})?;
// Serialize to JSON and send to client
let json = serde_json::to_string(&challenge)?;
println!("{}", json);
Ok(())
}CreateChallengeOptions Fields
| Field | Type | Default | Description |
|---|---|---|---|
algorithm | String | "PBKDF2/SHA-256" | KDF algorithm to use |
cost | u32 | 100_000 | Iteration/time cost (meaning varies by algorithm) |
counter | Option<u32> | None | Fixed counter for deterministic mode |
data | Option<BTreeMap<String, Value>> | None | Arbitrary metadata passed through to the client |
expires_at | Option<u64> | None | Unix timestamp after which challenge expires |
hmac_algorithm | HmacAlgorithm | Sha256 | HMAC algorithm for signing |
hmac_signature_secret | Option<String> | None | Secret for signing challenge parameters |
hmac_key_signature_secret | Option<String> | None | Secret for signing key prefix (deterministic mode) |
key_length | usize | 32 | Length of derived key in bytes |
key_prefix | String | "00" | Required prefix for the derived key |
key_prefix_length | Option<usize> | None | Override prefix length (default: key_length / 2) |
memory_cost | Option<u32> | None | Memory cost for Argon2id/Scrypt |
parallelism | Option<u32> | None | Parallelism for Argon2id/Scrypt |
Challenge Structure
The returned Challenge contains:
pub struct Challenge {
pub parameters: ChallengeParameters,
pub signature: Option<String>, // hex HMAC, present if hmac_signature_secret was set
}The ChallengeParameters holds all the fields needed by the client to solve the challenge:
pub struct ChallengeParameters {
pub algorithm: String, // e.g. "PBKDF2/SHA-256"
pub cost: u32,
pub data: Option<BTreeMap<String, Value>>,
pub expires_at: Option<u64>, // serialized as "expiresAt"
pub key_length: usize, // serialized as "keyLength"
pub key_prefix: String, // serialized as "keyPrefix"
pub key_signature: Option<String>, // serialized as "keySignature" (deterministic mode)
pub memory_cost: Option<u32>, // serialized as "memoryCost"
pub nonce: String,
pub parallelism: Option<u32>,
pub salt: String,
}2. Solving a Challenge
The client brute-forces a counter value until the derived key starts with the required prefix. Use solve_challenge() for this.
use altcha::{solve_challenge, SolveChallengeOptions, Challenge};
fn solve(challenge: &Challenge) -> altcha::Result<()> {
let solution = solve_challenge(SolveChallengeOptions::new(challenge))?
.expect("solution found within timeout");
println!("Counter: {}", solution.counter);
println!("Key: {}", solution.derived_key);
println!("Time: {:?} ms", solution.time);
Ok(())
}SolveChallengeOptions
pub struct SolveChallengeOptions<'a> {
pub challenge: &'a Challenge,
pub counter_start: u32, // default: 0
pub counter_step: u32, // default: 1
pub timeout_ms: u64, // default: 90_000 (90 seconds)
}- Use
SolveChallengeOptions::new(&challenge)for defaults. - Set
counter_startin deterministic mode to the pre-set counter value. - Increase
counter_stepto skip counters (e.g., step of 10 reduces precision but speeds up). - Reduce
timeout_msfor tighter time bounds on server-side solving.
Solution Structure
pub struct Solution {
pub counter: u32,
pub derived_key: String, // hex-encoded derived key
pub time: Option<f64>, // solving time in milliseconds
}3. Verifying a Solution
The server verifies the submitted solution by checking expiration, HMAC signature, and the correctness of the derived key.
use altcha::{verify_solution, VerifySolutionOptions};
fn verify(challenge: &altcha::Challenge, solution: &altcha::Solution) -> altcha::Result<()> {
let result = verify_solution(VerifySolutionOptions::new(
challenge,
solution,
"server-secret-key", // must match the hmac_signature_secret from creation
))?;
if result.verified {
println!("Legitimate user");
} else if result.expired {
println!("Challenge expired");
} else if result.invalid_signature == Some(true) {
println!("Challenge was tampered with");
} else if result.invalid_solution == Some(true) {
println!("Solution is incorrect");
}
Ok(())
}VerifySolutionOptions
pub struct VerifySolutionOptions<'a> {
pub challenge: &'a Challenge,
pub solution: &'a Solution,
pub hmac_algorithm: HmacAlgorithm, // default: Sha256
pub hmac_key_signature_secret: Option<String>, // for deterministic mode fast-path
pub hmac_signature_secret: String, // for challenge parameter signing
}Use VerifySolutionOptions::new(&challenge, &solution, "secret") for the simplest setup.
VerifySolutionResult
pub struct VerifySolutionResult {
pub verified: bool,
pub expired: bool,
pub invalid_signature: Option<bool>, // None if expired before this check
pub invalid_solution: Option<bool>, // None if signature check failed
pub time: f64, // verification time in milliseconds
}Verification happens in three steps:
1. Expiration check — If expires_at is set and the current time exceeds it, expired = true and verified = false. No further checks are performed. 2. Signature check — If the challenge was signed, the server re-computes the HMAC over the parameters and compares with challenge.signature using constant-time comparison. If this fails, invalid_signature = Some(true). 3. Solution check — Either the key signature is verified (fast path in deterministic mode) or the derived key is recomputed from the submitted counter and compared.
4. The Payload Structure
A Payload combines the challenge and solution into a single unit — this is what clients typically submit to the server.
pub struct Payload {
pub challenge: Challenge,
pub solution: Solution,
}Deserialize from the client's JSON body:
let payload: altcha::Payload = serde_json::from_str(&request_body)?;
let result = verify_solution(VerifySolutionOptions::new(
&payload.challenge,
&payload.solution,
"secret",
))?;5. Solving in Async Contexts
Since solve_challenge() is CPU-bound and synchronous, it can block the async runtime. Always wrap it in spawn_blocking:
#[tokio::main]
async fn main() -> altcha::Result<()> {
let challenge = create_challenge(CreateChallengeOptions::default())?;
let challenge_clone = challenge.clone();
let solution = tokio::task::spawn_blocking(move || {
solve_challenge(SolveChallengeOptions::new(&challenge_clone))
})
.await.expect("spawn_blocking failed")?;
let solution = solution.expect("solution found");
println!("Counter: {}", solution.counter);
Ok(())
}6. Signing Challenges Manually
If you need to build challenge parameters manually (e.g., for custom challenge generation), use sign_challenge():
use altcha::{sign_challenge, ChallengeParameters, HmacAlgorithm};
fn main() -> altcha::Result<()> {
let mut params = ChallengeParameters {
algorithm: "PBKDF2/SHA-256".to_string(),
cost: 5_000,
nonce: "random-nonce".to_string(),
salt: "random-salt".to_string(),
key_prefix: "00".to_string(),
key_length: 32,
data: None,
expires_at: None,
key_signature: None,
memory_cost: None,
parallelism: None,
};
sign_challenge(
&HmacAlgorithm::Sha256,
&mut params,
None, // no derived key for standard mode
"my-hmac-secret",
None, // no key signature for standard mode
)?;
println!("Signature: {:?}", params); // params now includes the signature
Ok(())
}Deterministic Mode — Fast-Path Verification
This reference covers deterministic challenges and key signatures, which allow the server to skip expensive key re-derivation during verification.
Overview
In standard mode, when verifying a solution, the server must re-derive the key from the submitted counter using the same KDF algorithm — this can take hundreds of milliseconds for high-cost challenges. Deterministic mode eliminates this by pre-computing the expected result.
In deterministic mode:
1. The server picks a specific counter value when creating the challenge 2. The server pre-computes what the key_prefix should be for that counter 3. The server signs this expected prefix with hmac_key_signature_secret 4. The key_signature is included in the challenge parameters sent to the client 5. The client still solves the challenge normally (finds the counter by brute force) 6. On verification, the server checks the key_signature instead of re-deriving the key — this is an O(1) HMAC comparison
When to Use Deterministic Mode
Use deterministic mode when:
- You need fast verification (sub-millisecond) for high-traffic endpoints
- Your challenge
costis high (e.g., 100,000+ PBKDF2 iterations), making re-derivation expensive - You want to reduce server CPU usage per verification
Use standard mode when:
- You need simplicity and don't care about verification speed
- Your challenge
costis low (e.g., 1,000-5,000) - You don't want to manage a second secret key
Creating a Deterministic Challenge
Set both counter and hmac_key_signature_secret:
use altcha::*;
use rand::Rng;
fn main() -> altcha::Result<()> {
// Pick a random counter in a reasonable range
// The range affects difficulty: higher counters take longer to reach by brute force
let counter = rand::rng().random_range(10_000..=100_000);
let challenge = create_challenge(CreateChallengeOptions {
algorithm: "PBKDF2/SHA-256".to_string(),
cost: 50_000,
counter: Some(counter),
expires_at: Some(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() + 600,
),
// Required: signs the challenge parameters
hmac_signature_secret: Some("my-hmac-secret".to_string()),
// Required for deterministic mode: signs the expected key prefix
hmac_key_signature_secret: Some("my-key-signature-secret".to_string()),
..Default::default()
})?;
// The challenge now includes a `key_signature` in its parameters
let json = serde_json::to_string(&challenge)?;
println!("Challenge with key signature: {}", json);
Ok(())
}What Happens Internally
When counter and hmac_key_signature_secret are both set, create_challenge():
1. Generates a random salt and nonce as usual 2. Derives the key using the given algorithm, salt, and counter 3. Extracts the prefix of length key_prefix_length from the derived key 4. Computes HMAC(prefix, hmac_key_signature_secret) to create the key_signature 5. Signs the challenge parameters with hmac_signature_secret (HMAC over the JSON) 6. The key_signature is embedded in ChallengeParameters.key_signature
Verifying in Deterministic Mode (Fast Path)
Pass hmac_key_signature_secret to verify_solution() to enable the fast path:
let result = verify_solution(VerifySolutionOptions {
hmac_key_signature_secret: Some("my-key-signature-secret".to_string()),
hmac_signature_secret: "my-hmac-secret".to_string(),
..VerifySolutionOptions::new(&challenge, &solution, "my-hmac-secret")
})?;When hmac_key_signature_secret is provided, verification:
1. Checks expiration (same as standard mode) 2. Verifies the challenge signature (same as standard mode) 3. Fast path: Derives the key prefix from the submitted counter, computes HMAC with hmac_key_signature_secret, and compares with challenge.parameters.key_signature — O(1) HMAC comparison 4. If the key signature matches, verified = true without any expensive KDF re-derivation
Comparison: Standard vs Deterministic Verification
| Aspect | Standard Mode | Deterministic Mode |
|---|---|---|
| Verification time | ~cost-dependent (50ms-5s) | ~0.1ms (constant) |
| Server CPU | High (re-derives full key) | Negligible (HMAC only) |
| Secrets required | hmac_signature_secret | Both secrets |
| Security | Re-derivation is mathematically sound | HMAC provides same assurance |
| Complexity | Simple | Requires counter range management |
Counter Range Selection
The counter value determines how much work the client must do before finding the solution. Since the client brute-forces from counter 0 upward, choosing counter = 50_000 means the client must perform ~50,000 key derivations on average.
Recommended ranges by use case:
| Use Case | Counter Range | Expected Solve Time (PBKDF2, cost=5,000) |
|---|---|---|
| Quick forms | 1,000 - 5,000 | ~0.5 - 2.5s |
| Standard forms | 5,000 - 50,000 | ~2.5 - 25s |
| High-value actions | 50,000 - 200,000 | ~25 - 100s |
| API protection | 100 - 1,000 | ~0.05 - 0.5s |
Note: These are approximate and depend heavily on client hardware. Mobile devices are significantly slower.
Using sign_challenge() Manually
If you need to build challenges with custom logic (e.g., custom nonce generation, external salt sources), use sign_challenge():
use altcha::{sign_challenge, ChallengeParameters, HmacAlgorithm};
use rand::Rng;
fn create_custom_deterministic_challenge() -> altcha::Result<ChallengeParameters> {
let mut params = ChallengeParameters {
algorithm: "PBKDF2/SHA-256".to_string(),
cost: 10_000,
nonce: rand::rng().random::<[u8; 16]>().iter().map(|b| format!("{:02x}", b)).collect(),
salt: rand::rng().random::<[u8; 16]>().iter().map(|b| format!("{:02x}", b)).collect(),
key_prefix: "00".to_string(),
key_length: 32,
data: None,
expires_at: None,
key_signature: None,
memory_cost: None,
parallelism: None,
};
// Sign the parameters (adds HMAC signature and optionally key signature)
sign_challenge(
&HmacAlgorithm::Sha256,
&mut params,
None, // no pre-computed derived_key needed — sign_challenge will compute it
"my-hmac-secret",
Some("my-key-signature-secret"), // enables deterministic mode
)?;
Ok(params)
}sign_challenge() Parameters
pub fn sign_challenge(
algorithm: &HmacAlgorithm,
parameters: &mut ChallengeParameters,
derived_key: Option<&[u8]>, // pre-computed derived key (optional)
hmac_signature_secret: &str, // signs the challenge parameters
hmac_key_signature_secret: Option<&str>, // signs the key prefix (enables deterministic)
) -> Result<()>- If
derived_keyis provided, the key prefix is extracted from it - If
derived_keyisNone, the key is derived fromparameters.counterusing the algorithm, salt, and cost - The HMAC signature is written to
parameters.signature(fromChallenge.signature) - The key signature is written to
parameters.key_signature
Two-Secret Architecture
Deterministic mode requires two separate secrets, each with a distinct purpose:
1. `hmac_signature_secret` — Signs the entire challenge parameters JSON. This prevents the client from modifying cost, algorithm, salt, etc. Used in both standard and deterministic mode.
2. `hmac_key_signature_secret` — Signs the expected key prefix. This enables the server to verify the solution without re-deriving the key. Only used in deterministic mode.
Both secrets should be cryptographically random, at least 32 bytes, stored in environment variables, and rotated periodically.
# .env
ALTCHA_HMAC_SECRET=a1b2c3d4e5f6... # 32+ random hex chars
ALTCHA_KEY_SECRET=f6e5d4c3b2a1... # 32+ random hex chars (deterministic mode only)Fields Hash Verification — Form Integrity Protection
This reference covers verify_fields_hash() and how ALTCHA protects form fields from client-side tampering using HMAC hashes.
Overview
ALTCHA provides a mechanism to verify that specific form fields have not been modified between when the server generated a form and when the client submitted it. This is useful for:
- Protecting hidden fields (e.g., product IDs, prices, recipient addresses) from tampering
- Ensuring email/username fields haven't been swapped
- Detecting JavaScript injection into form fields
- Complementing server signature verification from ALTCHA Sentinel
The server computes an HMAC hash of selected field values when rendering the form. This hash is sent to the client (typically via ALTCHA Sentinel's verificationData). On form submission, the server re-computes the hash and compares.
Function Signature
pub fn verify_fields_hash(
form_data: &HashMap<String, String>,
fields: &[String],
fields_hash: &str,
algorithm: Option<&HmacAlgorithm>,
) -> boolParameters
| Parameter | Type | Description |
|---|---|---|
form_data | &HashMap<String, String> | All submitted form field names and values |
fields | &[String] | List of field names whose values should be hashed (in order) |
fields_hash | &str | The expected HMAC hash (hex-encoded) to compare against |
algorithm | Option<&HmacAlgorithm> | Hash algorithm: &HmacAlgorithm::Sha256 (default), Sha384, or Sha512 |
How It Works Internally
1. Extract values from form_data for each field name in fields, in order 2. Join the values with "\n" as separator 3. Compute HMAC-SHA-256(joined_values, fields_hash_key) where fields_hash_key is derived from fields_hash itself 4. Compare the result with fields_hash using constant-time comparison
The function returns true if the hash matches (fields are untampered) and false otherwise.
Basic Usage
use altcha::{verify_fields_hash, HmacAlgorithm};
use std::collections::HashMap;
fn main() {
// Simulate form data submitted by the client
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());
// The hash was originally computed by the server (or Sentinel) when the form was generated
let expected_hash = "a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890";
// Verify that the listed fields haven't been modified
let fields_to_check = vec![
"email".to_string(),
"name".to_string(),
];
let is_valid = verify_fields_hash(
&form_data,
&fields_to_check,
expected_hash,
Some(&HmacAlgorithm::Sha256), // algorithm defaults to SHA-256
);
if is_valid {
println!("Fields are intact — form has not been tampered with");
} else {
println!("WARNING: Fields have been modified!");
}
}Usage with ALTCHA Sentinel
When using ALTCHA Sentinel, the verification_data includes a fields_hash field. After verifying the server signature, verify the fields hash separately:
use altcha::{verify_server_signature, verify_fields_hash, ServerSignaturePayload, HmacAlgorithm};
use std::collections::HashMap;
fn verify_sentinel_and_fields(
encoded_payload: &str,
form_data: &HashMap<String, String>,
hmac_secret: &str,
) -> altcha::Result<bool> {
// Decode and verify server signature
let decoded = base64_decode(encoded_payload);
let payload: ServerSignaturePayload = serde_json::from_slice(&decoded)?;
let result = verify_server_signature(&payload, hmac_secret)?;
if !result.verified {
return Ok(false);
}
// Check fields hash if available
if let Some(data) = &result.verification_data {
if let (Some(fields), Some(hash)) = (&data.fields, &data.fields_hash) {
if !verify_fields_hash(form_data, fields, hash, Some(&HmacAlgorithm::Sha256)) {
return Ok(false); // fields were tampered with
}
}
}
Ok(true)
}Field Value Ordering
The order of field names in the fields array matters. The values are joined in the order specified, so changing the order changes the hash:
// These produce different hashes:
verify_fields_hash(&form, &["email", "name"], hash1, None); // "user@example.com\nJohn Doe"
verify_fields_hash(&form, &["name", "email"], hash2, None); // "John Doe\nuser@example.com"Always use the same field order that was used when generating the hash.
Handling Missing Fields
If a field listed in fields does not exist in form_data, the value is treated as an empty string. This means a client could delete a field to change the hash:
let mut form = HashMap::new();
form.insert("email".to_string(), "user@example.com".to_string());
// "name" is missing
// This will compute the hash of "user@example.com\n" (name is empty)
let result = verify_fields_hash(&form, &["email".into(), "name".into()], expected_hash, None);To detect missing fields, check the form data before calling verify_fields_hash:
fn all_fields_present(form_data: &HashMap<String, String>, fields: &[String]) -> bool {
fields.iter().all(|f| form_data.contains_key(f))
}Multi-line Field Values
The join separator is \n (newline), so multi-line values are handled correctly:
let mut form = HashMap::new();
form.insert("address".to_string(), "123 Main St\nApt 4B\nNew York".to_string());
form.insert("name".to_string(), "John".to_string());
// The joined string would be: "John\n123 Main St\nApt 4B\nNew York"
verify_fields_hash(&form, &["name".into(), "address".into()], hash, None);Algorithm Selection
The algorithm parameter controls which hash function is used:
| Algorithm | Output Size | When to Use |
|---|---|---|
"SHA-256" (default) | 32 bytes / 64 hex chars | Standard — sufficient for form integrity |
"SHA-384" | 48 bytes / 96 hex chars | Extra security margin |
"SHA-512" | 64 bytes / 128 hex chars | Maximum security |
In practice, SHA-256 is the standard choice. The hash is an HMAC (keyed), so collision resistance of the underlying hash is less of a concern than for plain hashes.
Common Patterns
Protecting Hidden Price Fields
// When generating the form:
let mut form_for_hash = HashMap::new();
form_for_hash.insert("product_id".to_string(), "PROD-12345".to_string());
form_for_hash.insert("price".to_string(), "29.99".to_string());
let hash = compute_fields_hash(&form_for_hash, &["product_id", "price"]);
// Embed `hash` in the form as a hidden field
// When processing the submission:
let submitted_form = parse_form_data(&request_body);
let original_hash = submitted_form.get("fields_hash").unwrap();
if !verify_fields_hash(
&submitted_form,
&["product_id".to_string(), "price".to_string()],
original_hash,
None,
) {
return Err("Price tampering detected");
}Protecting Email Recipient Fields
let is_valid = verify_fields_hash(
&submitted_form,
&["to_email".to_string(), "subject".to_string()],
expected_hash,
None,
);
if !is_valid {
// Someone changed the recipient email or subject
return Err("Form integrity check failed");
}Note: The crate does not provide a compute_fields_hash() function — that computation is typically done by the ALTCHA Sentinel service or the JS widget. The Rust crate focuses on verification. If you need to compute hashes server-side, use the hmac and sha2 crates directly.
Frontend Integration — ALTCHA Widget & JavaScript API
This reference covers the JavaScript/frontend side of ALTCHA. While the Rust altcha crate handles server-side challenge creation and verification, the frontend uses the official ALTCHA JS widget (altcha-lib) to solve challenges and submit payloads. The Rust backend and JS widget are fully wire-compatible.
Architecture Overview
┌─────────────────────┐ ┌──────────────────────┐
│ Browser (JS) │ │ Rust Server │
│ │ GET │ │
│ altcha-widget │────────►│ create_challenge() │
│ │ JSON │ │
│ solve_challenge() │◄────────│ Challenge JSON │
│ │ │ │
│ user submits form │ POST │ │
│ │────────►│ verify_solution() │
│ Payload JSON │ │ or │
│ │ │ verify_server_sig() │
└─────────────────────┘ └──────────────────────┘The flow is:
1. The JS widget requests a challenge from your Rust backend 2. The widget solves the challenge in a Web Worker (non-blocking) 3. The solution is stored in a hidden form field (base64-encoded JSON) 4. When the user submits the form, the payload goes to your Rust server for verification
Installation
CDN (Easiest)
<!-- Load the widget script -->
<script src="https://cdn.jsdelivr.net/npm/altcha@latest/dist/altcha.min.js"></script>Or use a specific version for stability:
<script src="https://cdn.jsdelivr.net/npm/altcha@1.0.7/dist/altcha.min.js"></script>npm / yarn / pnpm
npm install altcha
# or
yarn add altcha
# or
pnpm add altchaThen import in your JS/TS:
import "altcha/dist/altcha.min.js";ES Module
<script type="module">
import "https://cdn.jsdelivr.net/npm/altcha@latest/dist/altcha.min.js";
</script>Basic HTML Form Usage
The simplest integration — drop the widget into any HTML form:
<form action="/api/submit" method="POST">
<label>Name: <input type="text" name="name" required /></label>
<label>Email: <input type="email" name="email" required /></label>
<label>Message: <textarea name="message" required></textarea></label>
<!-- ALTCHA widget — replaces a traditional CAPTCHA -->
<altcha-widget
challengeurl="/api/altcha/challenge"
name="altcha"
></altcha-widget>
<button type="submit">Send Message</button>
</form>
<script src="https://cdn.jsdelivr.net/npm/altcha@latest/dist/altcha.min.js"></script>The widget:
- Automatically fetches a challenge from
/api/altcha/challenge - Solves it in a background Web Worker
- Stores the base64-encoded payload in a hidden
<input name="altcha"> - Shows a checkbox for the user to verify they're human (no puzzle to solve)
- Prevents form submission until the challenge is solved
Widget Attributes
The <altcha-widget> Web Component accepts these attributes:
Required Attributes
| Attribute | Description |
|---|---|
challengeurl | URL of your Rust backend challenge endpoint (e.g., /api/altcha/challenge) |
Common Attributes
| Attribute | Default | Description |
|---|---|---|
name | "altcha" | Name of the hidden input field containing the payload |
action | — | URL for Sentinel verification (ALTCHA Sentinel API endpoint) |
payload | — | Pre-set payload (skip challenge solving, useful for server-side) |
auto | null | Auto-verify on page load, on focus, or on submit without checkbox |
blockspam | null | Enable ALTCHA Sentinel spam/blocklist analysis |
expire | — | Requested challenge expiration time in seconds |
floating | null | Show widget as a floating button |
footer | "powered by ALTCHA" | Footer text (set to empty string to hide) |
hidefooter | — | Hide the footer completely |
hidelogo | — | Hide the ALTCHA logo |
language | "auto" | Language code for i18n (e.g., "de", "fr", "uk") |
maxnumber | 1e6 | Maximum counter value for solver |
strings | — | Custom strings object for i18n (JSON string or JS object) |
debug | — | Enable debug logging in console |
Security Attributes
| Attribute | Description |
|---|---|
spamfilter | Enable client-side spam filtering (checks form field values) |
verifyurl | Override the URL used for verification (instead of form action) |
Style Attributes
| Attribute | Description |
|---|---|
class | CSS class name for styling the widget |
style | Inline styles |
Widget Example with All Common Options
<altcha-widget
challengeurl="/api/altcha/challenge"
name="altcha"
auto="onsubmit"
expire="300"
maxnumber="500000"
hidelogo
hidefooter
language="en"
strings='{"ariaLabel": "Spam protection", "verified": "Verified!"}'
></altcha-widget>Auto-Verification Modes
The auto attribute controls when verification happens without requiring the user to click a checkbox:
<!-- Verify immediately when the page loads -->
<altcha-widget
challengeurl="/api/altcha/challenge"
auto="onload"
></altcha-widget>
<!-- Verify when the user focuses on any form field -->
<altcha-widget
challengeurl="/api/altcha/challenge"
auto="onfocus"
></altcha-widget>
<!-- Verify when the user clicks the form submit button (no visible checkbox) -->
<altcha-widget
challengeurl="/api/altcha/challenge"
auto="onsubmit"
></altcha-widget>| Mode | Behavior | UX Impact |
|---|---|---|
No auto (default) | User must click the checkbox | Most control, slight friction |
auto="onload" | Solves challenge on page load | Zero friction, works in background |
auto="onfocus" | Solves when user interacts with form | Near-zero friction |
auto="onsubmit" | Solves when submit is clicked | Zero visible friction |
For most user-friendly forms, use auto="onfocus" or auto="onsubmit".
JavaScript API
The widget exposes methods and events through the DOM element:
Getting the Widget Element
const widget = document.querySelector("altcha-widget");Methods
// Programmatically verify (trigger challenge solving)
widget.verify();
// Get the current payload (base64-encoded JSON string)
const payload = widget.payload;
// or
const payload = widget.getPayload();
// Check if verified
const isVerified = widget.verified; // true or false
// Reset the widget (clear solution, fetch new challenge)
widget.reset();
// Destroy the widget instance
widget.destroy();Events
const widget = document.querySelector("altcha-widget");
// Fired when challenge solving succeeds
widget.addEventListener("verified", (ev) => {
console.log("Verified!", ev.detail.payload);
});
// Fired when verification fails or errors occur
widget.addEventListener("error", (ev) => {
console.error("ALTCHA error:", ev.detail.error);
});
// Fired when the challenge is being solved (progress)
widget.addEventListener("statechange", (ev) => {
console.log("State:", ev.detail.state);
// States: 'unverified', 'verifying', 'verified', 'error'
});Programmatic Usage (Without HTML Form)
You can use ALTCHA entirely via JavaScript — no <altcha-widget> element needed:
import { Altcha } from "altcha";
const altcha = new Altcha({
challengeurl: "/api/altcha/challenge",
auto: "onfocus",
});
// When the form is submitted, get the payload
form.addEventListener("submit", async (e) => {
e.preventDefault();
const payload = await altcha.getPayload();
if (!payload) {
console.error("ALTCHA not verified yet");
return;
}
// Append payload to form data
const formData = new FormData(form);
formData.append("altcha", payload);
// Submit to your Rust backend
const response = await fetch("/api/submit", {
method: "POST",
body: formData,
});
});React Integration
Functional Component
import { useEffect, useRef } from "react";
interface AltchaWidgetProps {
challengeurl: string;
name?: string;
auto?: string;
onVerified?: (payload: string) => void;
onError?: (error: string) => void;
}
export function AltchaWidget({
challengeurl,
name = "altcha",
auto,
onVerified,
onError,
}: AltchaWidgetProps) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
// Load the script if not already loaded
if (!customElements.get("altcha-widget")) {
const script = document.createElement("script");
script.src =
"https://cdn.jsdelivr.net/npm/altcha@latest/dist/altcha.min.js";
script.async = true;
document.head.appendChild(script);
}
const container = ref.current;
const widget = document.createElement("altcha-widget");
widget.setAttribute("challengeurl", challengeurl);
widget.setAttribute("name", name);
if (auto) {
widget.setAttribute("auto", auto);
}
widget.addEventListener("verified", (ev: any) => {
onVerified?.(ev.detail.payload);
});
widget.addEventListener("error", (ev: any) => {
onError?.(ev.detail.error);
});
container?.appendChild(widget);
return () => {
// @ts-ignore
widget.destroy?.();
container?.removeChild(widget);
};
}, [challengeurl, name, auto]);
return <div ref={ref} />;
}
// Usage in a form
export function ContactForm() {
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const response = await fetch("/api/submit", {
method: "POST",
body: formData,
});
};
return (
<form onSubmit={handleSubmit}>
<input type="email" name="email" required />
<textarea name="message" required />
<AltchaWidget
challengeurl="/api/altcha/challenge"
auto="onfocus"
onVerified={(payload) => console.log("Verified:", payload)}
onError={(error) => console.error("Error:", error)}
/>
<button type="submit">Submit</button>
</form>
);
}npm Package in React
npm install altchaimport { useEffect, useRef } from "react";
import "altcha/dist/altcha.min.css";
export function AltchaWidget() {
const ref = useRef<any>(null);
useEffect(() => {
if (ref.current) {
ref.current.verify(); // trigger on mount
}
}, []);
return (
<altcha-widget
ref={ref}
challengeurl="/api/altcha/challenge"
auto="onfocus"
name="altcha"
/>
);
}Vue 3 Integration
<template>
<form @submit.prevent="handleSubmit">
<input type="email" name="email" required />
<textarea name="message" required />
<altcha-widget
ref="altchaRef"
:challengeurl="challengeUrl"
auto="onfocus"
name="altcha"
@verified="onVerified"
@error="onError"
/>
<button type="submit">Submit</button>
</form>
</template>
<script setup>
import { ref, onMounted } from "vue";
const challengeUrl = "/api/altcha/challenge";
const altchaRef = ref(null);
onMounted(() => {
// Load script dynamically if not present
if (!customElements.get("altcha-widget")) {
const script = document.createElement("script");
script.src =
"https://cdn.jsdelivr.net/npm/altcha@latest/dist/altcha.min.js";
document.head.appendChild(script);
}
});
function onVerified(ev) {
console.log("Verified:", ev.detail.payload);
}
function onError(ev) {
console.error("Error:", ev.detail.error);
}
async function handleSubmit(e) {
const formData = new FormData(e.target);
const response = await fetch("/api/submit", {
method: "POST",
body: formData,
});
console.log("Response:", response.status);
}
</script>Next.js Integration
App Router (app/)
// app/components/AltchaWidget.tsx
"use client";
import { useEffect, useRef } from "react";
export function AltchaWidget() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const script = document.createElement("script");
script.src =
"https://cdn.jsdelivr.net/npm/altcha@latest/dist/altcha.min.js";
script.async = true;
document.head.appendChild(script);
const container = ref.current;
if (!container) return;
const widget = document.createElement("altcha-widget");
widget.setAttribute("challengeurl", "/api/altcha/challenge");
widget.setAttribute("auto", "onfocus");
widget.setAttribute("name", "altcha");
container.appendChild(widget);
return () => {
widget.remove();
};
}, []);
return <div ref={ref} />;
}API Route (Next.js → calls your Rust backend)
If your Rust server is separate from Next.js, create a proxy API route:
// app/api/altcha/challenge/route.ts
import { NextResponse } from "next/server";
export async function GET() {
// Proxy the challenge request to your Rust backend
const response = await fetch("http://localhost:3000/api/altcha/challenge");
const data = await response.json();
return NextResponse.json(data);
}Then point the widget at the Next.js route:
widget.setAttribute("challengeurl", "/api/altcha/challenge");Internationalization (i18n)
Built-in Languages
The widget supports many languages out of the box. Set the language attribute:
<altcha-widget
challengeurl="/api/altcha/challenge"
language="de"
></altcha-widget>Supported language codes include: ar, bg, cs, da, de, en, es, fa, fi, fr, he, hu, id, it, ja, ko, lv, nb, nl, pl, pt, ro, ru, sk, sl, sr, sv, th, tr, uk, vi, zh.
RTL Support
For right-to-left languages (Arabic, Hebrew, Persian):
<altcha-widget
challengeurl="/api/altcha/challenge"
language="ar"
></altcha-widget>The widget automatically detects RTL based on the language code.
Custom Strings
Override any string with the strings attribute (JSON format):
<altcha-widget
challengeurl="/api/altcha/challenge"
strings='{
"label": "Verify you are human",
"verified": "All good!",
"verifying": "Working on it...",
"error": "Something went wrong",
"ariaLabel": "Anti-spam verification"
}'
></altcha-widget>Available string keys: label, verified, verifying, error, waitAlert, ariaLabel.
Styling the Widget
Default CSS
Include the default stylesheet for proper widget appearance:
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/altcha@latest/dist/altcha.min.css"
/>Or with npm:
@import "altcha/dist/altcha.min.css";Custom CSS
The widget uses standard CSS classes. Override them:
/* Widget container */
altcha-widget {
--altcha-color-border: #d1d5db;
--altcha-color-bg: #f9fafb;
--altcha-color-text: #111827;
--altcha-width: 100%;
--altcha-max-width: 260px;
--altcha-height: auto;
--altcha-radius: 4px;
--altcha-border-width: 1px;
--altcha-font-size: 14px;
}
/* Hover state */
altcha-widget:hover {
--altcha-color-border: #9ca3af;
}
/* Verified state */
altcha-widget[data-state="verified"] {
--altcha-color-border: #22c55e;
}
/* Error state */
altcha-widget[data-state="error"] {
--altcha-color-border: #ef4444;
}Dark Mode
@media (prefers-color-scheme: dark) {
altcha-widget {
--altcha-color-border: #374151;
--altcha-color-bg: #1f2937;
--altcha-color-text: #f9fafb;
}
}Floating Widget
Show the widget as a floating button (common for SPA navigation):
<altcha-widget challengeurl="/api/altcha/challenge" floating></altcha-widget>The floating widget appears as a small button in the corner. Clicking it triggers verification. Once verified, it collapses.
Payload Format
The hidden input name="altcha" contains a base64-encoded JSON string. Your Rust backend must decode it:
PoW Payload (base64 → JSON)
{
"challenge": {
"parameters": {
"algorithm": "PBKDF2/SHA-256",
"cost": 5000,
"nonce": "abc123...",
"salt": "def456...",
"keyPrefix": "00",
"keyLength": 32,
"expiresAt": 1714000000
},
"signature": "hmac-hex-string..."
},
"solution": {
"counter": 42,
"derivedKey": "00abcdef...",
"time": 1234.5
}
}Sentinel Server Signature Payload (base64 → JSON)
{
"algorithm": "SHA-256",
"signature": "hmac-hex-string...",
"verificationData": "classification=GOOD&score=15&...",
"verified": true,
"apiKey": "your-api-key",
"id": "unique-id"
}Your Rust backend handles both via the #[serde(untagged)] enum pattern described in references/server-integration.md.
Code Challenge Mode
ALTCHA also supports code challenges (like "What is 2 + 3?") as an alternative to PoW for accessibility:
<altcha-widget
challengeurl="/api/altcha/challenge"
name="altcha"
test="code"
></altcha-widget>For the Rust backend, you would need to handle the code challenge differently — this is currently more oriented toward the JS library's built-in code challenge feature. The Rust crate focuses on PoW verification.
Complete Working Example (HTML + Rust)
Frontend (index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Contact Form</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/altcha@latest/dist/altcha.min.css"
/>
</head>
<body>
<form id="contactForm" action="/api/submit" method="POST">
<h2>Contact Us</h2>
<label>
Name:
<input type="text" name="name" required />
</label>
<br />
<label>
Email:
<input type="email" name="email" required />
</label>
<br />
<label>
Message:
<textarea name="message" rows="4" required></textarea>
</label>
<br />
<altcha-widget
challengeurl="/api/altcha/challenge"
name="altcha"
auto="onfocus"
hidelogo
strings='{"label": "Verify you are human", "verified": "Verified!"}'
></altcha-widget>
<br />
<button type="submit">Send Message</button>
</form>
<script src="https://cdn.jsdelivr.net/npm/altcha@latest/dist/altcha.min.js"></script>
<script>
document.getElementById("contactForm").addEventListener("submit", (e) => {
const widget = document.querySelector("altcha-widget");
if (!widget.verified) {
e.preventDefault();
alert("Please wait for verification to complete.");
}
});
</script>
</body>
</html>Backend (Rust/Axum) — corresponding server
See references/server-integration.md for the full Axum server code that handles the /api/altcha/challenge and /api/submit endpoints.
Server Integration — Web Framework Patterns
This reference shows how to integrate ALTCHA into Rust web frameworks. The crate provides building blocks — you compose your own endpoints and middleware.
Axum Integration
Axum is the most common Rust async web framework and the one used in the crate's official examples. Here is a complete working server with challenge creation and verification.
Cargo.toml
[dependencies]
altcha = { version = "0", features = ["argon2", "scrypt"] }
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tower-http = { version = "0.6", features = ["cors"] }
base64 = "0.22"Full Server Example
use altcha::{
create_challenge, verify_fields_hash, verify_server_signature, verify_solution,
Challenge, CreateChallengeOptions, HmacAlgorithm, Payload, ServerSignaturePayload,
SolveChallengeOptions, VerifySolutionOptions,
};
use axum::{
extract::State,
http::StatusCode,
response::IntoResponse,
routing::{get, post},
Json, Router,
};
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::Arc;
// The HMAC secret — store in env vars in production
const HMAC_SECRET: &str = "my-server-hmac-secret";
const KEY_SIGNATURE_SECRET: &str = "my-key-signature-secret";
#[derive(Clone)]
struct AppState {
hmac_secret: String,
key_signature_secret: String,
}
#[tokio::main]
async fn main() {
let state = Arc::new(AppState {
hmac_secret: HMAC_SECRET.to_string(),
key_signature_secret: KEY_SIGNATURE_SECRET.to_string(),
});
let app = Router::new()
.route("/api/altcha/challenge", get(create_challenge_handler))
.route("/api/submit", post(verify_handler))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
// GET /api/altcha/challenge — returns a signed PoW challenge
async fn create_challenge_handler(
State(state): State<Arc<AppState>>,
) -> Result<Json<Challenge>, StatusCode> {
let challenge = create_challenge(CreateChallengeOptions {
algorithm: "PBKDF2/SHA-256".to_string(),
cost: 5_000,
expires_at: Some(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
+ 600,
),
hmac_signature_secret: Some(state.hmac_secret.clone()),
hmac_key_signature_secret: Some(state.key_signature_secret.clone()),
..Default::default()
})
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(challenge))
}
// POST /api/submit — verifies a PoW payload or Sentinel server signature
async fn verify_handler(
State(state): State<Arc<AppState>>,
Json(payload): Json<AltchaPayload>,
) -> Result<impl IntoResponse, StatusCode> {
match payload {
// Client PoW payload
AltchaPayload::Pow(p) => {
let result = verify_solution(VerifySolutionOptions {
hmac_key_signature_secret: Some(state.key_signature_secret.clone()),
hmac_signature_secret: state.hmac_secret.clone(),
..VerifySolutionOptions::new(&p.challenge, &p.solution, &state.hmac_secret)
})
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if result.verified {
Ok((StatusCode::OK, "verified"))
} else if result.expired {
Ok((StatusCode::BAD_REQUEST, "expired"))
} else {
Ok((StatusCode::BAD_REQUEST, "failed"))
}
}
// ALTCHA Sentinel server signature
AltchaPayload::Sentinel(sp) => {
let result = verify_server_signature(&sp, &state.hmac_secret)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if result.verified {
if let Some(data) = &result.verification_data {
if data.classification.as_deref() == Some("BAD") {
return Ok((StatusCode::BAD_REQUEST, "bot detected"));
}
}
Ok((StatusCode::OK, "verified"))
} else {
Ok((StatusCode::BAD_REQUEST, "signature verification failed"))
}
}
}
}
// Untagged enum to handle both payload types from the client
#[derive(Deserialize)]
#[serde(untagged)]
enum AltchaPayload {
Pow(Payload),
Sentinel(ServerSignaturePayload),
}HTML Form Integration
On the frontend, use the official ALTCHA widget. It automatically calls your challenge endpoint and submits the payload:
<form action="/api/submit" method="POST">
<input type="text" name="email" required />
<input type="text" name="message" required />
<input type="hidden" name="altcha" />
<!-- ALTCHA widget auto-populates the hidden field -->
<script src="https://cdn.jsdelivr.net/npm/altcha@latest/dist/altcha.min.js"></script>
<altcha-widget
challengeurl="/api/altcha/challenge"
name="altcha"
></altcha-widget>
<button type="submit">Submit</button>
</form>Detecting Payload Type
The ALTCHA JS widget can produce two types of payloads in the same hidden field:
1. Client PoW payload — Contains challenge and solution fields (the standard Payload struct) 2. Sentinel server signature — Contains algorithm, signature, verificationData, and verified fields (the ServerSignaturePayload struct)
Use #[serde(untagged)] to handle both:
#[derive(Deserialize)]
#[serde(untagged)]
enum AltchaPayload {
Pow(Payload),
Sentinel(ServerSignaturePayload),
}This works because the two payloads have different JSON structures. Serde will try deserializing as Payload first; if the fields don't match, it falls back to ServerSignaturePayload.
Actix-web Pattern
The same logic applies to Actix-web with minor syntactic differences:
use actix_web::{web, App, HttpResponse, HttpServer, post, get};
use altcha::*;
struct AppState {
hmac_secret: String,
}
#[get("/challenge")]
async fn challenge_handler(data: web::Data<AppState>) -> HttpResponse {
match create_challenge(CreateChallengeOptions {
algorithm: "PBKDF2/SHA-256".to_string(),
cost: 5_000,
hmac_signature_secret: Some(data.hmac_secret.clone()),
..Default::default()
}) {
Ok(challenge) => HttpResponse::Ok().json(challenge),
Err(_) => HttpResponse::InternalServerError().finish(),
}
}
#[post("/verify")]
async fn verify_handler(
data: web::Data<AppState>,
body: web::Json<Payload>,
) -> HttpResponse {
match verify_solution(VerifySolutionOptions::new(
&body.challenge,
&body.solution,
&data.hmac_secret,
)) {
Ok(result) if result.verified => HttpResponse::Ok().json(serde_json::json!({"verified": true})),
Ok(result) if result.expired => HttpResponse::BadRequest().json(serde_json::json!({"error": "expired"})),
_ => HttpResponse::BadRequest().json(serde_json::json!({"error": "failed"})),
}
}Error Response Patterns
Return meaningful HTTP status codes to help the frontend handle errors:
| Scenario | Status Code | Body |
|---|---|---|
| Challenge created | 200 | Challenge JSON |
| Solution verified | 200 | {"verified": true} |
| Challenge expired | 400 | {"error": "expired"} |
| Invalid signature | 400 | {"error": "invalid_signature"} |
| Invalid solution | 400 | {"error": "invalid_solution"} |
| Missing payload | 400 | {"error": "missing_payload"} |
| Internal error | 500 | {"error": "internal_error"} |
Replay Attack Prevention
The altcha crate is stateless — verify_solution only checks cryptographic validity and expiration. A valid solution can be submitted any number of times within the expiration window.
The Attack
An attacker solves a challenge once, then reuses that same valid Payload to make thousands of requests, bypassing the PoW requirement entirely.
The Fix: Nonce Caching
After verifying a solution, cache the challenge's salt (the unique nonce) with a TTL matching your expiration window. Reject any subsequent submission with the same nonce.
Example with Redis:
async fn verify_handler(
State(state): State<Arc<AppState>>,
Json(payload): Json<AltchaPayload>,
) -> Result<impl IntoResponse, StatusCode> {
match payload {
AltchaPayload::Pow(p) => {
let result = verify_solution(VerifySolutionOptions {
hmac_key_signature_secret: Some(state.key_signature_secret.clone()),
hmac_signature_secret: state.hmac_secret.clone(),
..VerifySolutionOptions::new(&p.challenge, &p.solution, &state.hmac_secret)
})
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if !result.verified {
return Ok((StatusCode::BAD_REQUEST, "failed"));
}
if result.expired {
return Ok((StatusCode::BAD_REQUEST, "expired"));
}
// Cache the salt nonce with NX (only if not exists) and 10-minute TTL
let is_new = redis::cmd("SET")
.arg(&p.challenge.salt)
.arg("1")
.arg("NX")
.arg("EX")
.arg(600)
.query_async::<_, bool>(&mut state.redis)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if !is_new {
return Ok((StatusCode::BAD_REQUEST, "replay attack"));
}
Ok((StatusCode::OK, "verified"))
}
// ... Sentinel handling remains the same
}
}Example with Moka (in-memory):
use moka::sync::Cache;
struct AppState {
hmac_secret: String,
key_signature_secret: String,
used_nonces: Cache<String, ()>,
}
impl AppState {
fn new() -> Self {
Self {
hmac_secret: HMAC_SECRET.to_string(),
key_signature_secret: KEY_SIGNATURE_SECRET.to_string(),
used_nonces: Cache::builder()
.max_capacity(10_000)
.time_to_live(std::time::Duration::from_secs(600))
.build(),
}
}
}
async fn verify_handler(
State(state): State<Arc<AppState>>,
Json(payload): Json<AltchaPayload>,
) -> Result<impl IntoResponse, StatusCode> {
match payload {
AltchaPayload::Pow(p) => {
let result = verify_solution(VerifySolutionOptions {
hmac_key_signature_secret: Some(state.key_signature_secret.clone()),
hmac_signature_secret: state.hmac_secret.clone(),
..VerifySolutionOptions::new(&p.challenge, &p.solution, &state.hmac_secret)
})
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if !result.verified {
return Ok((StatusCode::BAD_REQUEST, "failed"));
}
if result.expired {
return Ok((StatusCode::BAD_REQUEST, "expired"));
}
// Try to insert the salt into the cache — if it already exists, reject
if state.used_nonces.insert(p.challenge.salt.clone(), ()).is_some() {
return Ok((StatusCode::BAD_REQUEST, "replay attack"));
}
Ok((StatusCode::OK, "verified"))
}
// ... Sentinel handling remains the same
}
}Key Points
- Use the `salt` field as the cache key — it's the unique random nonce for each challenge
- Set TTL to match your `expires_at` window — the cache entry should expire at the same time the challenge expires
- Use `NX` flag in Redis (or equivalent) — ensures atomic insert-if-not-exists behavior to prevent race conditions
- For Sentinel payloads — the replay protection applies to
ServerSignaturePayloadfields; cache thesignaturefield instead ofsalt
Environment Variable Pattern for Secrets
Never hardcode secrets in production:
use std::env;
fn get_hmac_secret() -> String {
env::var("ALTCHA_HMAC_SECRET")
.expect("ALTCHA_HMAC_SECRET environment variable must be set")
}
fn get_key_signature_secret() -> Option<String> {
env::var("ALTCHA_KEY_SIGNATURE_SECRET").ok()
}ALTCHA Sentinel Server Signature Verification
This reference covers integrating ALTCHA Sentinel — a server-side spam analysis service that provides cryptographically signed verification results. Your Rust backend verifies these signatures to trust the analysis without direct API calls.
Overview
ALTCHA Sentinel is a paid/optional service that analyzes form submissions for spam patterns, bot behavior, and other threat signals. It returns a base64-encoded JSON payload containing a cryptographic signature. Your server verifies this signature to confirm the analysis is authentic and hasn't been tampered with.
The flow is:
1. The ALTCHA JS widget sends form data to ALTCHA Sentinel servers for analysis 2. Sentinel returns a signed ServerSignaturePayload embedded in a hidden form field 3. Your Rust backend receives this payload along with the form submission 4. Your backend verifies the signature using a shared HMAC secret 5. Based on the verified classification/score, you accept or reject the submission
ServerSignaturePayload Structure
pub struct ServerSignaturePayload {
pub algorithm: String, // e.g. "SHA-256"
pub api_key: Option<String>, // your ALTCHA API key (serde: "apiKey")
pub id: Option<String>, // unique request ID
pub signature: String, // hex HMAC of HASH(verificationData)
pub verification_data: String, // URL-encoded verification data (serde: "verificationData")
pub verified: bool, // whether Sentinel itself verified the payload
}The verification_data field is a URL-encoded string containing all the analysis results. The signature field is the hex-encoded HMAC of SHA256(verification_data_bytes) using your HMAC secret.
Verification Function
pub fn verify_server_signature(
payload: &ServerSignaturePayload,
hmac_secret: &str,
) -> Result<VerifyServerSignatureResult>Verification steps (performed internally):
1. Compute HMAC(SHA-256(verification_data_bytes), hmac_secret) 2. Compare the result with payload.signature using constant-time comparison 3. Parse the verification_data URL-encoded string 4. Check the expire field (if present) against current time 5. Verify both payload.verified and parsed verified are true
Result Structure
pub struct VerifyServerSignatureResult {
pub verified: bool, // overall verification result
pub expired: bool, // whether the payload has expired
pub invalid_signature: bool, // whether the HMAC signature didn't match
pub invalid_solution: bool, // whether the payload.verified was false
pub time: f64, // verification time in milliseconds
pub verification_data: Option<ServerSignatureVerificationData>, // parsed data
}Basic Usage
use altcha::{verify_server_signature, ServerSignaturePayload};
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
fn verify_sentinel(encoded_payload: &str) -> altcha::Result<bool> {
// 1. Decode the base64 payload from the hidden form field
let decoded = BASE64.decode(encoded_payload)
.map_err(|e| altcha::Error::InvalidParameters(format!("base64 decode error: {}", e)))?;
let payload: ServerSignaturePayload = serde_json::from_slice(&decoded)?;
// 2. Verify with your HMAC secret
let result = verify_server_signature(&payload, "your-sentinel-hmac-secret")?;
// 3. Check the result
if result.verified {
// You can inspect the verification_data for detailed analysis
if let Some(data) = &result.verification_data {
println!("Classification: {:?}", data.classification);
println!("Score: {:?}", data.score);
println!("Email analysis: {:?}", data.email);
}
Ok(true)
} else {
Ok(false)
}
}ServerSignatureVerificationData Fields
After verification, the parsed verification_data provides rich analysis:
pub struct ServerSignatureVerificationData {
pub classification: Option<String>, // "GOOD", "BAD", or "NEUTRAL"
pub email: Option<String>, // email classification or validation result
pub expire: Option<u64>, // Unix timestamp when analysis expires
pub fields: Option<Vec<String>>, // list of analyzed form field names
pub fields_hash: Option<String>, // HMAC hash of field values
pub id: Option<String>, // unique analysis ID
pub ip_address: Option<String>, // client IP address
pub reasons: Option<Vec<String>>, // reasons for the classification
pub score: Option<f64>, // bot probability score
pub time: Option<f64>, // analysis timestamp
pub verified: Option<bool>, // whether the analysis was verified
pub extra: BTreeMap<String, String>, // any additional/custom fields
}Classification Values
| Classification | Meaning | Action |
|---|---|---|
"GOOD" | Submission appears legitimate | Accept |
"NEUTRAL" | No strong signals either way | Accept (or flag for review) |
"BAD" | Likely spam or bot | Reject or challenge |
Score Interpretation
The score ranges from 0 to 100:
- 0-30: Very likely legitimate
- 31-60: Uncertain — depends on other signals
- 61-100: Increasingly likely to be spam/bot
Combined Verification (PoW + Sentinel)
In production, your form endpoint likely handles both PoW payloads and Sentinel server signatures. Use an untagged enum to accept both:
use altcha::{Payload, ServerSignaturePayload, verify_solution, verify_server_signature};
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(untagged)]
enum AltchaPayload {
Pow(Payload),
Sentinel(ServerSignaturePayload),
}
fn handle_submission(body: &str, hmac_secret: &str) -> bool {
// Try to detect payload type from the raw JSON
// Sentinel payloads contain "verificationData", PoW payloads contain "challenge"
if body.contains("verificationData") {
// It's a Sentinel payload
let decoded = base64_decode(&extract_payload_value(body));
if let Ok(payload) = serde_json::from_slice::<ServerSignaturePayload>(&decoded) {
if let Ok(result) = verify_server_signature(&payload, hmac_secret) {
return result.verified;
}
}
false
} else {
// It's a PoW payload
if let Ok(payload) = serde_json::from_str::<Payload>(body) {
if let Ok(result) = verify_solution(
VerifySolutionOptions::new(&payload.challenge, &payload.solution, hmac_secret)
) {
return result.verified;
}
}
false
}
}A cleaner approach is the #[serde(untagged)] enum pattern shown in references/server-integration.md.
Parsing Verification Data Independently
You can parse the verification_data string without full signature verification, for inspection or logging:
use altcha::parse_verification_data;
fn inspect_verification_data(verification_data: &str) {
if let Some(data) = parse_verification_data(verification_data) {
println!("Classification: {:?}", data.classification);
println!("Score: {:?}", data.score);
println!("Reasons: {:?}", data.reasons);
println!("IP: {:?}", data.ip_address);
println!("Email: {:?}", data.email);
// Access extra custom fields
for (key, value) in &data.extra {
println!("{}: {}", key, value);
}
}
}Note: parse_verification_data() parses the URL-encoded string but does NOT verify the signature. Always use verify_server_signature() for actual security decisions.
Fields Hash in Server Signatures
The verification_data may include a fields_hash — this is an HMAC hash of selected form field values. Use verify_fields_hash() to confirm the form data hasn't been tampered with since Sentinel analyzed it:
use altcha::{verify_fields_hash, HmacAlgorithm};
use std::collections::HashMap;
fn verify_sentinel_fields(
form_data: &HashMap<String, String>,
fields: &[String],
fields_hash: &str,
) -> bool {
verify_fields_hash(form_data, fields, fields_hash, Some(&HmacAlgorithm::Sha256))
}See references/fields-hash.md for full details on fields hash verification.
Types, Errors, and Serialization Reference
This reference provides a complete catalog of all public types, error variants, and serialization behavior in the altcha crate.
Public Enums
HmacAlgorithm
Controls which HMAC variant is used for signing and verification.
pub enum HmacAlgorithm {
#[serde(rename = "SHA-256")]
#[default]
Sha256,
#[serde(rename = "SHA-384")]
Sha384,
#[serde(rename = "SHA-512")]
Sha512,
}Derives: Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default
The serde rename ensures JSON uses the standard algorithm names ("SHA-256", "SHA-384", "SHA-512") rather than Rust enum variant names.
Error
All fallible functions return Result<T, Error>.
pub enum Error {
UnsupportedAlgorithm(String),
Hex(FromHexError),
Json(serde_json::Error),
InvalidHmacKey,
InvalidParameters(String),
}Derives: Debug, thiserror::Error Implements: std::error::Error, Display
| Variant | Meaning | When It Occurs |
|---|---|---|
UnsupportedAlgorithm | The algorithm string is not recognized | Passing an unknown algorithm like "BLAKE3" |
Hex | Invalid hex string in input | Malformed hex in salt, signature, or derived_key |
Json | JSON serialization/deserialization failed | Malformed JSON in payload |
InvalidHmacKey | HMAC key has invalid length | HMAC secret is empty or wrong length |
InvalidParameters | Challenge parameters are inconsistent | Missing required fields or invalid combinations |
Type Alias
pub type Result<T> = std::result::Result<T, Error>;Public Structs
CreateChallengeOptions
Configuration for create_challenge().
pub struct CreateChallengeOptions {
pub algorithm: String,
pub counter: Option<u32>,
pub cost: u32,
pub data: Option<BTreeMap<String, Value>>,
pub expires_at: Option<u64>,
pub hmac_algorithm: HmacAlgorithm,
pub hmac_key_signature_secret: Option<String>,
pub hmac_signature_secret: Option<String>,
pub key_length: usize,
pub key_prefix: String,
pub key_prefix_length: Option<usize>,
pub memory_cost: Option<u32>,
pub parallelism: Option<u32>,
}Implements: Default
Default values:
algorithm:"PBKDF2/SHA-256"cost:100_000key_length:32key_prefix:"00"hmac_algorithm:HmacAlgorithm::Sha256- All
Optionfields:None
Challenge
A signed challenge ready to send to the client.
pub struct Challenge {
pub parameters: ChallengeParameters,
pub signature: Option<String>,
}Derives: Debug, Clone, Serialize, Deserialize
signatureisSome(hex_string)whenhmac_signature_secretwas provided during creationsignatureisNonefor unsigned challenges (not recommended for production)- Serde:
signatureusesskip_serializing_if = "Option::is_none"
ChallengeParameters
The algorithmic parameters the client needs to solve the challenge.
pub struct ChallengeParameters {
pub algorithm: String,
pub cost: u32,
pub data: Option<BTreeMap<String, Value>>,
pub expires_at: Option<u64>,
pub key_length: usize,
pub key_prefix: String,
pub key_signature: Option<String>,
pub memory_cost: Option<u32>,
pub nonce: String,
pub parallelism: Option<u32>,
pub salt: String,
}Derives: Debug, Clone, Serialize, Deserialize
Serde field name mappings (camelCase for JS compatibility):
key_length→"keyLength"key_prefix→"keyPrefix"key_signature→"keySignature"expires_at→"expiresAt"memory_cost→"memoryCost"
Optional fields use skip_serializing_if = "Option::is_none" — they are omitted from JSON when None.
Solution
The client's solution to a challenge.
pub struct Solution {
pub counter: u32,
pub derived_key: String,
pub time: Option<f64>,
}Derives: Debug, Clone, Serialize, Deserialize
counter: The counter value that produced a matching key prefixderived_key: Hex-encoded derived key (starts with the challenge'skey_prefix)time: Solving time in milliseconds (may beNoneif not measured)
Serde: derived_key → "derivedKey"
Payload
Combines challenge and solution — the complete client submission.
pub struct Payload {
pub challenge: Challenge,
pub solution: Solution,
}Derives: Debug, Clone, Serialize, Deserialize
SolveChallengeOptions
Configuration for solve_challenge().
pub struct SolveChallengeOptions<'a> {
pub challenge: &'a Challenge,
pub counter_start: u32,
pub counter_step: u32,
pub timeout_ms: u64,
}Constructor: SolveChallengeOptions::new(&challenge) sets defaults:
counter_start:0counter_step:1timeout_ms:90_000(90 seconds)
VerifySolutionOptions
Configuration for verify_solution().
pub struct VerifySolutionOptions<'a> {
pub challenge: &'a Challenge,
pub solution: &'a Solution,
pub hmac_algorithm: HmacAlgorithm,
pub hmac_key_signature_secret: Option<String>,
pub hmac_signature_secret: String,
}Constructor: VerifySolutionOptions::new(&challenge, &solution, "secret") sets defaults:
hmac_algorithm:HmacAlgorithm::Sha256hmac_key_signature_secret:Nonehmac_signature_secret: the provided string
VerifySolutionResult
Outcome of solution verification.
pub struct VerifySolutionResult {
pub verified: bool,
pub expired: bool,
pub invalid_signature: Option<bool>,
pub invalid_solution: Option<bool>,
pub time: f64,
}Derives: Debug, Clone
invalid_signatureisNoneif the challenge expired before signature checkinginvalid_solutionisNoneif the signature check already failed (short-circuit)
ServerSignaturePayload
Payload from ALTCHA Sentinel containing a signed analysis result.
pub struct ServerSignaturePayload {
pub algorithm: String,
pub api_key: Option<String>,
pub id: Option<String>,
pub signature: String,
pub verification_data: String,
pub verified: bool,
}Derives: Debug, Clone, Serialize, Deserialize
Serde mappings: api_key → "apiKey", verification_data → "verificationData"
ServerSignatureVerificationData
Parsed verification data from a Sentinel payload.
pub struct ServerSignatureVerificationData {
pub classification: Option<String>, // "GOOD", "BAD", or "NEUTRAL"
pub email: Option<String>, // email classification or validation result
pub expire: Option<u64>, // Unix timestamp when analysis expires
pub fields: Option<Vec<String>>, // list of analyzed form field names
pub fields_hash: Option<String>, // HMAC hash of field values
pub id: Option<String>, // unique analysis ID
pub ip_address: Option<String>, // client IP address
pub reasons: Option<Vec<String>>, // reasons for the classification
pub score: Option<f64>, // bot probability score
pub time: Option<f64>, // analysis timestamp
pub verified: Option<bool>, // whether the analysis was verified
pub extra: BTreeMap<String, String>, // any additional/custom fields
}Derives: Debug, Clone, Default, Serialize
- Uses
#[serde(flatten)]forextra— any unrecognized fields are captured here ip_address→"ipAddress"fields_hash→"fieldsHash"
VerifyServerSignatureResult
Outcome of server signature verification.
pub struct VerifyServerSignatureResult {
pub verified: bool,
pub expired: bool,
pub invalid_signature: bool,
pub invalid_solution: bool,
pub time: f64,
pub verification_data: Option<ServerSignatureVerificationData>,
}Derives: Debug, Clone
Public Functions Summary
// Challenge lifecycle
pub fn create_challenge(options: CreateChallengeOptions) -> Result<Challenge>
pub fn solve_challenge(options: SolveChallengeOptions<'_>) -> Result<Option<Solution>>
pub fn verify_solution(options: VerifySolutionOptions<'_>) -> Result<VerifySolutionResult>
pub fn sign_challenge(
algorithm: &HmacAlgorithm,
parameters: &mut ChallengeParameters,
derived_key: Option<&[u8]>,
hmac_signature_secret: &str,
hmac_key_signature_secret: Option<&str>,
) -> Result<()>
// Server signature (Sentinel)
pub fn verify_server_signature(
payload: &ServerSignaturePayload,
hmac_secret: &str,
) -> Result<VerifyServerSignatureResult>
pub fn verify_fields_hash(
form_data: &HashMap<String, String>,
fields: &[String],
fields_hash: &str,
algorithm: Option<&HmacAlgorithm>,
) -> bool
pub fn parse_verification_data(data: &str) -> Option<ServerSignatureVerificationData>Serialization Notes
Wire Format Compatibility
All types serialize to camelCase JSON matching the JavaScript ALTCHA library. This enables seamless interop between a Rust backend and a JS frontend:
// Rust
let challenge = create_challenge(options)?;
let json = serde_json::to_string(&challenge)?;
// JavaScript can parse this directly:
// const data = JSON.parse(json);
// console.log(data.parameters.keyLength); // 32
// console.log(data.parameters.keyPrefix); // "00"
// console.log(data.signature); // hex HMAC stringDeserialization
All challenge/solution types implement Deserialize. This is important for accepting payloads from the JavaScript ALTCHA widget:
// The JS widget sends this JSON structure:
let json = r#"{
"challenge": {
"parameters": {
"algorithm": "PBKDF2/SHA-256",
"cost": 5000,
"nonce": "abc123",
"salt": "def456",
"keyPrefix": "00",
"keyLength": 32
},
"signature": "abcdef0123456789..."
},
"solution": {
"counter": 42,
"derivedKey": "00abcdef...",
"time": 1234.5
}
}"#;
let payload: Payload = serde_json::from_str(json)?;Error Handling Patterns
// Pattern 1: Unwrap with expect (for examples/tests)
let challenge = create_challenge(options).expect("challenge creation failed");
// Pattern 2: Match on specific errors
match create_challenge(options) {
Ok(challenge) => { /* use challenge */ },
Err(Error::UnsupportedAlgorithm(algo)) => {
eprintln!("Unknown algorithm: {}", algo);
},
Err(Error::InvalidHmacKey) => {
eprintln!("HMAC secret is invalid — check its length");
},
Err(e) => {
eprintln!("Failed to create challenge: {}", e);
},
}
// Pattern 3: Map to HTTP responses (in web frameworks)
fn create_challenge_endpoint() -> impl IntoResponse {
match create_challenge(options) {
Ok(challenge) => (StatusCode::OK, Json(challenge)).into_response(),
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "error").into_response(),
}
}