ALTCHA Python Library

March 30, 2026 · View on GitHub

The ALTCHA Python Library is a lightweight, zero-dependency library designed for creating and verifying ALTCHA challenges, specifically tailored for Python applications.

Compatibility

  • Python 3.9+

Example

Installation

pip install altcha

For Argon2id support (optional):

pip install altcha argon2-cffi

Tests

python -m unittest discover tests

PoW v2

PoW v2 replaces the simple hash-matching approach of v1 with a key derivation function (KDF) proof of work. Instead of finding a number whose hash equals a target, the client must find a counter value whose derived key starts with a required prefix. This enables memory-hard algorithms (Argon2id, scrypt) that are more resistant to GPU/ASIC attacks.

Algorithms

Algorithm stringKDFNotes
'SHA-256', 'SHA-384', 'SHA-512'Iterated SHAFast, for testing / low-security use
'PBKDF2/SHA-256', 'PBKDF2/SHA-384', 'PBKDF2/SHA-512'PBKDF2Good default
'SCRYPT'scryptMemory-hard
'ARGON2ID'Argon2idMemory-hard, requires argon2-cffi

Quick start

from altcha import (
    create_challenge,
    solve_challenge,
    verify_solution,
    Payload,
)

HMAC_SECRET = "secret hmac key"

# Server: create a challenge
challenge = create_challenge(
    algorithm="PBKDF2/SHA-256",
    cost=5_000,
    hmac_secret=HMAC_SECRET,
)

# Client: solve the challenge
solution = solve_challenge(challenge)
if solution is None:
    raise RuntimeError("Challenge could not be solved in time")

# Client: encode and transmit the payload
payload_b64 = Payload(challenge, solution).to_base64()

# Server: verify
result = verify_solution(payload_b64, HMAC_SECRET)
print(result.verified)   # True

Deterministic mode

Pass a counter to create_challenge to pre-solve the challenge. The derived key prefix is embedded in the challenge so the client must find exactly that counter. Combine with hmac_key_secret to enable fast server-side verification without re-deriving the key.

import secrets

counter = secrets.randbelow(5_000) + 5_000

challenge = create_challenge(
    algorithm="PBKDF2/SHA-256",
    cost=5_000,
    counter=counter,
    hmac_secret=HMAC_SECRET,
    hmac_key_secret="key-signing-secret",
)

solution = solve_challenge(challenge)
if solution is None:
    raise RuntimeError("Challenge could not be solved in time")
payload_b64 = Payload(challenge, solution).to_base64()

result = verify_solution(
    payload_b64,
    HMAC_SECRET,
    hmac_key_secret="key-signing-secret",  # enables fast path
)
print(result.verified)  # True

Expiry

import datetime

challenge = create_challenge(
    algorithm="PBKDF2/SHA-256",
    cost=5_000,
    expires_at=datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=10),
    hmac_secret=HMAC_SECRET,
)

Custom derive_key

Pass your own derive_key function to use a custom or third-party KDF:

def my_derive_key(parameters, salt: bytes, password: bytes) -> bytes:
    ...

challenge = create_challenge(
    algorithm="MY-ALGO",
    cost=1,
    derive_key=my_derive_key,
    hmac_secret=HMAC_SECRET,
)

PoW v1 (legacy)

The original ALTCHA proof of work. The client brute-forces a number n such that hash(salt + n) == challenge. Available under the _v1 / V1 suffix.


API reference

V2

create_challenge(algorithm, cost, *, derive_key, counter, key_length, key_prefix, key_prefix_length, memory_cost, parallelism, expires_at, data, hmac_secret, hmac_key_secret, hmac_algorithm) → Challenge

Create a new v2 proof-of-work challenge.

ParameterTypeDefaultDescription
algorithmstrKDF algorithm identifier (e.g. 'PBKDF2/SHA-256', 'ARGON2ID', 'SCRYPT', 'SHA-256').
costintAlgorithm-specific cost (iterations / passes).
derive_keycallableauto(parameters, salt: bytes, password: bytes) -> bytes. Defaults to built-in for the algorithm.
counterintNonePre-solve with this counter (deterministic mode).
key_lengthint32Derived key length in bytes.
key_prefixstr'00'Hex prefix the derived key must start with.
key_prefix_lengthintkey_length // 2Bytes of the derived key used as prefix in deterministic mode.
memory_costintNoneMemory cost in KiB (Argon2id / scrypt).
parallelismintNoneParallelism factor (Argon2id / scrypt).
expires_atint | datetimeNoneExpiry as a Unix timestamp or datetime.
datadictNoneArbitrary metadata embedded in the challenge.
hmac_secretstrNoneSecret for signing the challenge. If omitted, challenge is unsigned.
hmac_key_secretstrNoneSecret for signing the derived key (fast verification path).
hmac_algorithmstr'SHA-256'HMAC digest algorithm.

Returns Challenge.


solve_challenge(challenge, derive_key, *, counter_start, counter_step, timeout) → Solution | None

Solve a v2 challenge by brute-forcing counter values.

ParameterTypeDefaultDescription
challengeChallengeThe challenge to solve.
derive_keycallableautoKDF function. Defaults to built-in for the algorithm.
counter_startint0First counter value to try.
counter_stepint1Increment between attempts (use > 1 for partitioned parallel solving).
timeoutfloat90.0Maximum seconds to spend. Returns None on timeout.

Returns Solution or None.


verify_solution(payload, hmac_secret, derive_key, *, hmac_key_secret, hmac_algorithm) → VerifySolutionResult

Verify a v2 challenge solution.

ParameterTypeDefaultDescription
payloadstr | PayloadBase64-encoded JSON string or Payload object.
hmac_secretstrSecret used to verify the challenge signature.
derive_keycallableautoKDF function for re-derivation.
hmac_key_secretstrNoneSecret for the fast verification path.
hmac_algorithmstr'SHA-256'HMAC digest algorithm.

Returns VerifySolutionResult with fields:

FieldTypeDescription
verifiedboolTrue if the solution is valid.
expiredboolTrue if the challenge has expired.
invalid_signaturebool | NoneTrue if the challenge signature is missing or wrong.
invalid_solutionbool | NoneTrue if the solution is incorrect.
timefloatTime taken for verification in milliseconds.
errorstr | NoneSet if the payload could not be parsed.

Built-in derive_key functions

FunctionAlgorithm
derive_key_sha(parameters, salt, password)Iterated SHA (SHA-256/384/512)
derive_key_pbkdf2(parameters, salt, password)PBKDF2 (SHA-256/384/512)
derive_key_scrypt(parameters, salt, password)scrypt
derive_key_argon2id(parameters, salt, password)Argon2id (requires argon2-cffi)

Server Signature Verification

verify_fields_hash(form_data, fields, fields_hash, algorithm) → bool

Verifies the hash of specific form fields.

verify_server_signature(payload, hmac_key) → (bool, ServerSignatureVerificationData | None, str | None)

Verifies an ALTCHA server signature.


V1 (legacy)

create_challenge_v1(options) → ChallengeV1

Creates a new v1 challenge.

ChallengeOptionsV1 parameters:

ParameterTypeDefaultDescription
algorithmstr'SHA-256'Hashing algorithm ('SHA-1', 'SHA-256', 'SHA-512').
max_numberint1,000,000Upper bound for the random number.
salt_lengthint12Length of the random salt in bytes.
hmac_keystrRequired HMAC key.
saltstrautoOptional salt. Random if omitted.
numberintautoOptional number. Random if omitted.
expiresdatetimeNoneOptional expiration time.
paramsdictNoneOptional URL-encoded query parameters appended to the salt.

verify_solution_v1(payload, hmac_secret, check_expires) → (bool, str | None)

Verifies a v1 solution payload.

solve_challenge_v1(challenge, salt, algorithm, max_number, start) → SolutionV1 | None

Brute-forces a v1 challenge.

extract_params_v1(payload) → dict

Extracts URL parameters from the payload's salt.


License

MIT