diff --git a/api/create-challenge.mdx b/api/create-challenge.mdx index 27727e4..c3befd5 100644 --- a/api/create-challenge.mdx +++ b/api/create-challenge.mdx @@ -48,7 +48,7 @@ You can call `createChallenge()` in either of two styles: - Memory-hardness profile, only valid with `algorithm: 'argon2id'`. The profile abstracts the raw Argon2id parameters (`m`, `t`, `p`) so you never pass memory sizes yourself. Passing `argonProfile` with `algorithm: 'sha256'` throws. + Memory-hardness profile, only valid with `algorithm: 'argon2id'`. The profile abstracts the raw Argon2id parameters (`m`, `t`, `p`) so you never pass memory sizes yourself. Both profiles currently resolve to the same conservative tuning; pick the tier that matches your audience now, and future retuning will not break in-flight tokens because each token embeds its own parameters. Passing `argonProfile` with `algorithm: 'sha256'` throws. @@ -271,12 +271,16 @@ Key differences from SHA-256: - **Difficulty scale.** Each Argon2id hash takes milliseconds instead of microseconds, so difficulty caps at `8` and `"auto"` bounds default to `1`–`2` (versus `64` and `3`–`6` for SHA-256). - **Profiles instead of raw parameters.** `argonProfile` resolves the Argon2id memory, iteration, and parallelism parameters for you. The library enforces a hard upper bound (`HARD_MAX`, exported from `ribaunt`) on those parameters, and tokens carrying values above it are rejected as `invalid-token`. -- **Tokens are self-describing.** Each challenge token carries its algorithm and Argon2id parameters, signed into the JWT. `verifySolution()` detects the algorithm per token, so your verify endpoint needs no changes. +- **Tokens are self-describing.** Each challenge token carries its algorithm, its Argon2id parameters, and a construction version (`v: 1`), signed into the JWT. `verifySolution()` detects the algorithm per token, so your verify endpoint needs no changes, and future profile retuning cannot break tokens that are already in flight. - **Browser support is automatic.** The widget and its solver worker detect the algorithm per token and load the Argon2id solver on demand. The [`solver-backend` event](/widget/events#solver-backend) reports `argon2id` so you can confirm it in telemetry. - **Testing.** The synchronous [`solveChallenge()`](/api/solve-challenge) helper supports SHA-256 only. Use `solveChallengeAsync()` in tests that solve `argon2id` tokens. The adaptive engine works the same for both algorithms: `riskScore` and `calibration` remain raise-only signals within your configured bounds. [`assess()`](/api/assess) accepts `algorithm` and `argonProfile` in its `workload` options when you want the risk engine to produce Argon2id workloads. + + Argon2id narrows the attacker's GPU and ASIC advantage but does not eliminate it. A well-funded adversary can still solve any proof-of-work challenge, just at a higher memory cost. Treat it as one abuse-cost layer alongside rate limiting and risk signals, not as human verification or the sole gate for sensitive actions. + + ## Validation `createChallenge()` validates its inputs at runtime and throws if anything is invalid: diff --git a/api/types.mdx b/api/types.mdx index 6333e4a..d1e5eee 100644 --- a/api/types.mdx +++ b/api/types.mdx @@ -29,7 +29,7 @@ interface ChallengeSolution { `PowAlgorithm` selects the proof-of-work algorithm for `createChallenge()` and `selectWorkload()`. The default is `'sha256'`; `'argon2id'` is a memory-hard opt-in — see [Argon2id opt-in](/api/create-challenge#argon2id-opt-in). -`ArgonProfile` abstracts the raw Argon2id parameters so you never pass memory sizes yourself. It is only valid when the algorithm is `'argon2id'`. +`ArgonProfile` abstracts the raw Argon2id parameters so you never pass memory sizes yourself. It is only valid when the algorithm is `'argon2id'`. Both profiles currently resolve to the same conservative parameters (`m: 8192`, `t: 1`, `p: 1`, `hashLen: 32`). Because each token embeds its own parameters and a construction version, choosing a tier now is safe: retuning in a future release cannot break in-flight tokens. ```ts type PowAlgorithm = 'sha256' | 'argon2id'; diff --git a/api/verify-solution.mdx b/api/verify-solution.mdx index 2d7f4e1..0c657e7 100644 --- a/api/verify-solution.mdx +++ b/api/verify-solution.mdx @@ -148,7 +148,7 @@ const result = await verifySolution(tokens, solutions, { | Reason | Description | | --- | --- | -| `invalid-token` | JWT signature is invalid or malformed | +| `invalid-token` | JWT signature is invalid, the payload is malformed, or the token carries an unknown construction version (`v`) | | `expired-token` | Challenge TTL has passed | | `invalid-solution` | The nonce does not produce a valid hash | | `context-mismatch` | The supplied context does not match the token's bound context | diff --git a/changelog/overview.mdx b/changelog/overview.mdx index b04b95c..6614d35 100644 --- a/changelog/overview.mdx +++ b/changelog/overview.mdx @@ -4,6 +4,22 @@ description: "New features, updates, and fixes to Ribaunt." mode: "center" --- + + ## Improvements + + **Challenge token construction version** + + Every challenge token now embeds a construction version (`v: 1`) that pins how the solution is computed. [`verifySolution`](/api/verify-solution) rejects tokens with an unknown future version as `invalid-token`, and the solvers return `undefined` instead of solving under wrong assumptions. Tokens issued before this release (no `v` field) are treated as version 1 and still verify, so no action is needed. + + **Argon2id profile tiers clarified** + + The `'mobile'` and `'standard'` values of [`argonProfile`](/api/create-challenge#argon2id-opt-in) currently share the same conservative tuning. Pick the tier that matches your audience now; because each token embeds its own parameters and version, future retuning cannot break in-flight tokens. + + ## Fixes + + - **Argon2id salt entropy.** Challenge strings now carry 16 random bytes (128 bits) of entropy, and the Argon2id salt is derived from the full-entropy challenge instead of a shorter zero-padded value. Tokens issued before this fix still verify during their TTL window. + + ## New diff --git a/concepts/how-it-works.mdx b/concepts/how-it-works.mdx index af972e4..d594d5e 100644 --- a/concepts/how-it-works.mdx +++ b/concepts/how-it-works.mdx @@ -55,10 +55,13 @@ Each challenge token is a standard signed JWT. When decoded, its payload contain | Field | Description | | --- | --- | -| `challenge` | A random base64 string generated fresh for each token | +| `challenge` | A random 128-bit base64url string (16 random bytes) generated fresh for each token | | `difficulty` | The number of leading zeros the SHA-256 hash must start with | | `expires` | A Unix timestamp (seconds) after which the token is rejected | | `jti` | A UUID that uniquely identifies this token, used for replay prevention | +| `v` | The construction version, currently `1`. It pins how the solution is computed, so future changes cannot break in-flight tokens. | + +Tokens with an unknown future `v` value fail closed: `verifySolution()` rejects them as `invalid-token`, and the solvers return `undefined` instead of solving under wrong assumptions. Tokens issued before versioning (no `v` field) are treated as version 1 and still verify. Ribaunt hashes with SHA-256 by default. If you opt in to the memory-hard [Argon2id algorithm](/api/create-challenge#argon2id-opt-in), each token additionally carries the algorithm identifier and its Argon2id parameters, and the solver and `verifySolution()` detect the algorithm per token automatically.