C++17 library and CLI implementing the ALTCHA Proof-of-Work v2 protocol.
ALTCHA PoW v2 is a brute-force counter-based key derivation challenge. A solver iterates a counter, combines it with a nonce, feeds it into a KDF alongside a salt, and checks the resulting key against a required hex prefix. The first counter whose derived key starts with that prefix is the solution.
Supported KDF algorithms:
| Algorithm | Identifier |
|---|---|
| SHA-256 / SHA-384 / SHA-512 | SHA-256, SHA-384, SHA-512 |
| PBKDF2-HMAC-SHA-256/384/512 | PBKDF2/SHA-256, PBKDF2/SHA-384, PBKDF2/SHA-512 |
| Scrypt | SCRYPT |
| Argon2id | ARGON2ID |
- OpenSSL (libcrypto) — SHA, PBKDF2, Scrypt, HMAC
- libargon2 — Argon2id
- C++17 standard library (threads)
- nlohmann/json (bundled as
third_party/json.hpp)
macOS (Homebrew):
brew install openssl argon2Debian / Ubuntu:
apt install libssl-dev libargon2-devWindows (vcpkg):
vcpkg install openssl argon2
cmake -B build -DCMAKE_TOOLCHAIN_FILE=<vcpkg-root>/scripts/buildsystems/vcpkg.cmakecmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build buildThis produces:
build/libaltcha.a— static librarybuild/altcha— CLI
ctest --test-dir build -Vinclude(FetchContent)
FetchContent_Declare(
altcha
GIT_REPOSITORY https://github.com/altcha-org/altcha-lib-cpp.git
GIT_TAG main
)
FetchContent_MakeAvailable(altcha)
target_link_libraries(your_target PRIVATE altcha)add_subdirectory(third_party/altcha-lib-cpp)
target_link_libraries(your_target PRIVATE altcha)Install to the system (or a custom prefix):
cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local
cmake --build build
cmake --install buildThen in your project:
find_package(altcha REQUIRED)
target_link_libraries(your_target PRIVATE altcha::altcha)Note: when using find_package, link against altcha::altcha (namespaced target).
#include <altcha/altcha.hpp>Include the single public header:
#include <altcha/altcha.hpp>Link against libaltcha.a, OpenSSL (-lcrypto), and Argon2 (-largon2).
altcha::CreateChallengeOptions opts;
opts.algorithm = "PBKDF2/SHA-256";
opts.cost = 5000;
opts.hmacSignatureSecret = "server-secret"; // signs the challenge
opts.expiresAt = time(nullptr) + 600; // expires in 10 minutes
altcha::Challenge ch = altcha::createChallenge(opts);
std::string json = altcha::challengeToJson(ch, /*pretty=*/true);Deterministic mode — pre-solve at a known counter (useful for testing or server-side difficulty tuning):
opts.counter = 5000; // key prefix will be the derived key at this counter
altcha::Challenge ch = altcha::createChallenge(opts);Fast-path verification — sign the derived key so verification doesn't need to re-run the KDF:
opts.hmacKeySignatureSecret = "key-secret";
altcha::Challenge ch = altcha::createChallenge(opts);
// ch.parameters.keySignature is now populatedSingle-threaded:
altcha::Challenge ch = altcha::challengeFromJson(jsonString);
altcha::SolveChallengeOptions solveOpts;
solveOpts.timeout = 90000; // ms, 0 = no timeout
std::optional<altcha::Solution> sol = altcha::solveChallenge(ch, solveOpts);
if (sol) {
std::cout << altcha::solutionToJson(*sol, true) << "\n";
}Multi-threaded:
// 0 = use hardware_concurrency()
std::optional<altcha::Solution> sol = altcha::solveChallengeWorkers(ch, /*numThreads=*/0);Aborting early:
volatile bool abort = false;
// set abort = true from another thread or signal handler to stop the solver
auto sol = altcha::solveChallenge(ch, {}, &abort);altcha::VerifyResult result = altcha::verifySolution(
ch, *sol,
"server-secret", // hmacSignatureSecret (required)
"key-secret" // hmacKeySignatureSecret (optional, enables fast path)
);
if (result.verified) {
// accepted
} else if (result.expired) {
// challenge expired
} else if (result.invalidSignature) {
// challenge was tampered with
} else if (result.invalidSolution) {
// wrong counter / derived key
}When using the ALTCHA Sentinel, the payload contains a signed ServerSignaturePayload. Use verifyServerSignature to validate it on your server.
// Decode the payload from JSON (e.g. sent by the client from the API response)
altcha::ServerSignaturePayload payload =
altcha::serverSignaturePayloadFromJson(jsonString);
altcha::VerifyServerSignatureResult result =
altcha::verifyServerSignature(payload, "hmac-secret");
if (result.verified) {
// Signature is valid and the solution was accepted
auto& vd = result.verificationData;
if (vd.score.has_value())
std::cout << "spam score: " << *vd.score << "\n";
} else if (result.expired) {
// verificationData.expire is in the past
} else if (result.invalidSignature) {
// HMAC mismatch – wrong secret or tampered payload
} else if (result.invalidSolution) {
// payload.verified == false or verificationData.verified == false
}VerifyServerSignatureResult extends the basic VerifyResult fields with:
| Field | Type | Description |
|---|---|---|
hasVerificationData |
bool |
true when verificationData was parsed successfully |
verificationData |
ServerSignatureVerificationData |
parsed fields from the URL-encoded payload |
ServerSignatureVerificationData fields:
| Field | Type |
|---|---|
classification |
optional<string> |
email |
optional<string> |
expire |
optional<int64_t> (unix seconds) |
fields |
optional<vector<string>> |
fieldsHash |
optional<string> |
id |
optional<string> |
ipAddress |
optional<string> |
reasons |
optional<vector<string>> |
score |
optional<double> |
time |
optional<int64_t> |
verified |
optional<bool> |
extra |
map<string, string> (unknown keys) |
Parse a URL-encoded verificationData string independently:
altcha::ServerSignatureVerificationData vd =
altcha::parseVerificationData("verified=true&expire=1735689600&score=1.5");When the ALTCHA widget is configured to hash form fields, verify the hash server-side:
std::map<std::string, std::string> form = {
{"email", "user@example.com"},
{"message", "hello"},
};
bool ok = altcha::verifyFieldsHash(
form,
*payload.verificationData.fields, // e.g. {"email", "message"}
*payload.verificationData.fieldsHash,
"SHA-256" // default, matches widget configuration
);// Challenge
altcha::Challenge ch = altcha::challengeFromJson(str);
std::string str = altcha::challengeToJson(ch, /*pretty=*/false);
// Solution
altcha::Solution sol = altcha::solutionFromJson(str);
std::string str = altcha::solutionToJson(sol, /*pretty=*/false);
// Server signature payload
altcha::ServerSignaturePayload p = altcha::serverSignaturePayloadFromJson(str);altcha <challenge.json> [--workers N]--workers defaults to 1. Set a higher value to use multiple threads.
altcha challenge.json # single worker (default)
altcha challenge.json --workers 4 # use 4 workersOutput (JSON to stdout):
{
"counter": 1000,
"derivedKey": "bb88101a4ee7fa94f960da326245942087e7ad4c2ed14ff13dca5eba53264d2a",
"time": 41.5
}altcha --create [options]| Option | Description | Default |
|---|---|---|
--algorithm ALGO |
KDF algorithm | PBKDF2/SHA-256 |
--cost N |
KDF cost / iterations | 50000 |
--key-length N |
Derived key length in bytes | 32 |
--memory-cost N |
Memory cost (Scrypt/Argon2) | 0 |
--parallelism N |
Parallelism (Scrypt/Argon2) | 0 |
--expires-in N |
Expiry in seconds from now | none |
--secret S |
HMAC secret for challenge signature | none |
--key-secret S |
HMAC secret for key signature | none |
--counter N |
Deterministic mode: pre-solve at counter N | none |
altcha --create \
--algorithm PBKDF2/SHA-256 \
--cost 5000 \
--expires-in 600 \
--secret server-secret \
--counter 5000altcha <challenge.json> --verify <solution.json> --secret <hmac_secret> [--key-secret <key_secret>]altcha challenge.json --verify solution.json --secret server-secretOutput (JSON to stdout):
{
"verified": true,
"expired": false,
"invalidSignature": false,
"invalidSolution": false,
"time": 2.3
}MIT