altcha
A Rust implementation of the ALTCHA Proof-of-Work v2 protocol.
ALTCHA is a privacy-friendly, self-hosted CAPTCHA alternative that uses proof-of-work challenges to block bots.
Installation
[]
= "0"
To enable optional KDF algorithms:
[]
= { = "0", = ["argon2", "scrypt"] }
Examples
Features
| Feature | Enables | Default |
|---|---|---|
argon2 |
Argon2id algorithm | no |
scrypt |
scrypt algorithm | no |
PBKDF2 and iterative SHA algorithms are always available.
Quick start
use ;
// ── Server: create a challenge ───────────────────────────────────────────────
let challenge = create_challenge?;
// Serialize and send `challenge` to the client (e.g. as JSON).
// ── Client: solve the challenge ──────────────────────────────────────────────
let solution = solve_challenge?
.expect;
// Send `solution` back to the server alongside `challenge`.
// ── Server: verify the solution ──────────────────────────────────────────────
let result = verify_solution?;
assert!;
API
create_challenge
Generates a new challenge with a random 16-byte nonce and salt. If hmac_signature_secret is set the challenge is signed with HMAC so that the server can detect tampering on verification.
CreateChallengeOptions fields (all optional except algorithm and cost):
| Field | Type | Default | Description |
|---|---|---|---|
algorithm |
String |
— | KDF algorithm string (see Algorithms) |
cost |
u32 |
— | Algorithm cost (iterations, time cost, N for scrypt) |
counter |
Option<u32> |
None |
Enables deterministic mode; key prefix is derived from this counter |
data |
Option<BTreeMap<String, Value>> |
None |
Arbitrary metadata embedded in the signed challenge |
expires_at |
Option<u64> |
None |
Unix timestamp (seconds) after which the challenge is invalid |
hmac_algorithm |
HmacAlgorithm |
Sha256 |
HMAC digest algorithm |
hmac_signature_secret |
Option<String> |
None |
Secret for signing the challenge; if absent the challenge is unsigned |
hmac_key_signature_secret |
Option<String> |
None |
Secret for signing the derived key (deterministic mode only) |
key_length |
usize |
32 |
Output key length in bytes |
key_prefix |
String |
"00" |
Required hex prefix the derived key must start with |
key_prefix_length |
Option<usize> |
key_length / 2 |
Bytes used as prefix in deterministic mode |
memory_cost |
Option<u32> |
None |
Memory cost in KiB (Argon2id, scrypt r) |
parallelism |
Option<u32> |
None |
Parallelism factor (Argon2id, scrypt p; default 1) |
solve_challenge
Iterates counter values from counter_start, incrementing by counter_step, until the derived key starts with the required prefix. Returns None when timeout_ms elapses.
SolveChallengeOptions fields:
| Field | Default | Description |
|---|---|---|
challenge |
— | Reference to the challenge to solve |
counter_start |
0 |
First counter value to try |
counter_step |
1 |
Increment per attempt |
timeout_ms |
90_000 |
Maximum solve time in milliseconds |
Use SolveChallengeOptions::new(&challenge) to get sensible defaults.
verify_solution
Verifies a submitted solution in three steps:
- Expiration — rejects challenges whose
expires_athas passed. - Signature — recomputes
HMAC(canonical_json(parameters), secret)and compares in constant time. - Solution — either verifies the submitted key against a stored key signature (fast path, deterministic mode) or re-derives the key from the submitted counter (full path).
VerifySolutionOptions fields:
| Field | Default | Description |
|---|---|---|
challenge |
— | The original challenge |
solution |
— | The solution submitted by the client |
hmac_signature_secret |
— | Secret used when the challenge was created |
hmac_algorithm |
Sha256 |
HMAC digest algorithm |
hmac_key_signature_secret |
None |
Secret for fast-path key signature verification |
Use VerifySolutionOptions::new(&challenge, &solution, "secret") for defaults.
VerifySolutionResult:
sign_challenge
Signs an existing set of challenge parameters. Useful when building challenges manually.
verify_server_signature
Verifies a payload issued by ALTCHA Sentinel. The payload is typically obtained from a form field (base64-encoded JSON) when using the ALTCHA Sentinel service for server-side bot scoring.
Verification steps:
- Compute
HMAC(SHA(verificationData), hmac_secret)and compare withpayload.signaturein constant time. - Parse
verificationData(URL-encoded query string) into typed fields. - Check that the
expiretimestamp has not passed. - Check that both
payload.verifiedand the parsedverifiedfield aretrue.
ServerSignaturePayload fields:
| Field | Type | Description |
|---|---|---|
algorithm |
String |
Hash and HMAC algorithm (e.g. "SHA-256") |
api_key |
Option<String> |
ALTCHA Sentinel API key (informational) |
id |
Option<String> |
Submission ID |
signature |
String |
Hex-encoded HMAC(SHA(verificationData), secret) |
verification_data |
String |
URL-encoded query string from ALTCHA Sentinel |
verified |
bool |
Whether Sentinel considers the submission verified |
VerifyServerSignatureResult:
ServerSignatureVerificationData — parsed fields from verificationData:
| Field | Type | Description |
|---|---|---|
classification |
Option<String> |
"BAD", "GOOD", or "NEUTRAL" |
email |
Option<String> |
Submitter email (if provided) |
expire |
Option<u64> |
Unix timestamp after which the payload expires |
fields |
Option<Vec<String>> |
Form field names included in the fields hash |
fields_hash |
Option<String> |
Hex hash of the selected form fields |
id |
Option<String> |
Submission ID |
ip_address |
Option<String> |
Submitter IP address |
reasons |
Option<Vec<String>> |
Scoring reasons |
score |
Option<f64> |
Bot probability score |
time |
Option<f64> |
Submission timestamp |
verified |
Option<bool> |
Whether Sentinel verified the submission |
extra |
BTreeMap<String, String> |
Any additional fields not listed above |
Example:
use ;
// The ALTCHA widget puts a base64-encoded JSON ServerSignaturePayload
// into a hidden form field when connected to ALTCHA Sentinel.
let payload: ServerSignaturePayload = from_slice?;
let result = verify_server_signature?;
if result.verified
verify_fields_hash
Verifies that a hash of selected form field values matches an expected digest. Used to confirm that specific fields have not been tampered with after the ALTCHA Sentinel payload was signed.
Joins the values of fields (in the given order) with "\n", hashes with the specified algorithm (defaults to SHA-256), and compares with fields_hash in constant time.
parse_verification_data
Parses the URL-encoded verificationData string from a [ServerSignaturePayload] into a typed [ServerSignatureVerificationData] struct. Called automatically by verify_server_signature; exposed for cases where you need to inspect the data independently.
Algorithms
| Algorithm string | KDF | Feature | Notes |
|---|---|---|---|
"PBKDF2/SHA-256" |
PBKDF2-HMAC-SHA-256 | — | Default; cost = iteration count |
"PBKDF2/SHA-384" |
PBKDF2-HMAC-SHA-384 | — | cost = iteration count |
"PBKDF2/SHA-512" |
PBKDF2-HMAC-SHA-512 | — | cost = iteration count |
"SHA-256" |
Iterative SHA-256 | — | cost = iteration count |
"SHA-384" |
Iterative SHA-384 | — | cost = iteration count |
"SHA-512" |
Iterative SHA-512 | — | cost = iteration count |
"SCRYPT" |
scrypt | scrypt |
cost = N (must be power of 2), memory_cost = r (default 8), parallelism = p (default 1) |
"ARGON2ID" |
Argon2id | argon2 |
cost = time cost, memory_cost = KiB (required), parallelism = p (default 1) |
Serialization
Challenge, Solution, and ServerSignaturePayload implement serde::Serialize / serde::Deserialize and produce JSON compatible with the reference JavaScript library. Field names follow the camelCase convention used in the ALTCHA spec (keyLength, keyPrefix, expiresAt, derivedKey, verificationData, etc.).
let json = to_string?;
let challenge: Challenge = from_str?;
License
MIT