Skip to content

Repository files navigation

ALTCHA Python Library

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 string KDF Notes
'SHA-256', 'SHA-384', 'SHA-512' Iterated SHA Fast, for testing / low-security use
'PBKDF2/SHA-256', 'PBKDF2/SHA-384', 'PBKDF2/SHA-512' PBKDF2 Good default
'SCRYPT' scrypt Memory-hard
'ARGON2ID' Argon2id Memory-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.

Parameter Type Default Description
algorithm str — KDF algorithm identifier (e.g. 'PBKDF2/SHA-256', 'ARGON2ID', 'SCRYPT', 'SHA-256').
cost int — Algorithm-specific cost (iterations / passes).
derive_key callable auto (parameters, salt: bytes, password: bytes) -> bytes. Defaults to built-in for the algorithm.
counter int None Pre-solve with this counter (deterministic mode).
key_length int 32 Derived key length in bytes.
key_prefix str '00' Hex prefix the derived key must start with.
key_prefix_length int key_length // 2 Bytes of the derived key used as prefix in deterministic mode.
memory_cost int None Memory cost in KiB (Argon2id / scrypt).
parallelism int None Parallelism factor (Argon2id / scrypt).
expires_at int | datetime None Expiry as a Unix timestamp or datetime.
data dict None Arbitrary metadata embedded in the challenge.
hmac_secret str None Secret for signing the challenge. If omitted, challenge is unsigned.
hmac_key_secret str None Secret for signing the derived key (fast verification path).
hmac_algorithm str '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.

Parameter Type Default Description
challenge Challenge — The challenge to solve.
derive_key callable auto KDF function. Defaults to built-in for the algorithm.
counter_start int 0 First counter value to try.
counter_step int 1 Increment between attempts (use > 1 for partitioned parallel solving).
timeout float 90.0 Maximum 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.

Parameter Type Default Description
payload str | Payload — Base64-encoded JSON string or Payload object.
hmac_secret str — Secret used to verify the challenge signature.
derive_key callable auto KDF function for re-derivation.
hmac_key_secret str None Secret for the fast verification path.
hmac_algorithm str 'SHA-256' HMAC digest algorithm.

Returns VerifySolutionResult with fields:

Field Type Description
verified bool True if the solution is valid.
expired bool True if the challenge has expired.
invalid_signature bool | None True if the challenge signature is missing or wrong.
invalid_solution bool | None True if the solution is incorrect.
time float Time taken for verification in milliseconds.
error str | None Set if the payload could not be parsed.

Built-in derive_key functions

Function Algorithm
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.

verify_server(payload, url, secret, *, headers, timeout, retries, retry_delay, retry_backoff, http_post) → VerifyServerResult

Verifies a payload remotely via the ALTCHA Sentinel /v1/verify/signature API, instead of checking the HMAC signature locally. Avoids managing the HMAC secret on your server, at the cost of a network round-trip.

from altcha import verify_server

result = verify_server(
    payload,  # the payload received from POST /v1/verify
    url="https://sentinel.example.com/v1/verify/signature",
    secret=API_KEY_SECRET,  # optional, checked against the payload's API key
    timeout=10,
    retries=2,
)

if result.verified:
    ...
Parameter Type Default Description
payload str | ServerSignaturePayload | dict — The payload to verify.
url str — Full URL of the Sentinel /v1/verify/signature endpoint.
secret str None API key secret, checked against the payload's API key.
headers dict None Additional headers to send with the request.
timeout float 10 Per-attempt request timeout in seconds.
retries int 0 Number of retry attempts after the first try.
retry_delay float 0.3 Base delay in seconds between retries.
retry_backoff str 'exponential' 'fixed' or 'exponential' backoff.
http_post callable stdlib urllib Transport override: (url, body, headers, timeout) -> (status, body).

Returns VerifyServerResult with fields:

Field Type Description
verified bool True if Sentinel confirmed the payload is valid.
reason str | None Failure reason, if any.
api_key str | None The API key associated with the payload, if returned.
verification_data dict | None Parsed verification data, if returned.

V1 (legacy)

create_challenge_v1(options) → ChallengeV1

Creates a new v1 challenge.

ChallengeOptionsV1 parameters:

Parameter Type Default Description
algorithm str 'SHA-256' Hashing algorithm ('SHA-1', 'SHA-256', 'SHA-512').
max_number int 1,000,000 Upper bound for the random number.
salt_length int 12 Length of the random salt in bytes.
hmac_key str — Required HMAC key.
salt str auto Optional salt. Random if omitted.
number int auto Optional number. Random if omitted.
expires datetime None Optional expiration time.
params dict None Optional 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

About

A lightweight Python library for creating and verifying ALTCHA challenges.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

33 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages