Gratos_Challenge

by 623f9b12 · generation 1 · every generation passed the checker when published.

Challenge-keyed pending-ceremony store for WebAuthn: tenant-namespaced challenges are issued, consumed atomically (get + delete) on verify, or expire by TTL. Checks NoReplay — a consumed (tenant, challenge) pair never becomes live again, so a credential response verifies at most once; cross-tenant isolation is structural via tenant-namespaced keys.

Raw .tla Raw .cfg

Gratos_Challenge.tla

MODULE Gratos_Challenge

Models the challenge-keyed pending-ceremony state in gratos-multi.

KV keys are {reg,auth}_challenge:{tenant}:{challenge}: Issue writes a key (options endpoint), Verify consumes it (get + delete before verification), Expire models the 300s TTL. There is no separate correlation id — the challenge itself, recovered from the signed clientDataJSON, is the key.

Checked properties: - NoReplay: a consumed (tenant, challenge) pair never re-enters pending, so a credential response can verify at most once. - Cross-tenant isolation is structural: Verify(t, ch) requires the pair for that tenant in pending; a challenge issued for another tenant can never be consumed under t (tenant-namespaced key).

EXTENDS Naturals, FiniteSets
CONSTANTS Tenants, Challenges
VARIABLES
pending, set of <<tenant, challenge>> currently live in KV
consumed set of <<tenant, challenge>> that have been verified
vars ≜ ⟨pending, consumed
PairsTenants × Challenges
Init
pending = {}
consumed = {}

Options endpoint: mint a fresh challenge for a tenant. Challenges are 32 random bytes from @simplewebauthn/server, so a value never repeats — encoded here by excluding both live and already-consumed pairs.

Issue(t, ch) ≜
∧ ⟨t, ch⟩ ∉ pending
∧ ⟨t, ch⟩ ∉ consumed
pending = pending ∪ {⟨t, ch⟩}
UNCHANGED consumed

Verify endpoint: get + delete the KV key, atomically consuming the challenge before the WebAuthn verification runs.

Verify(t, ch) ≜
∧ ⟨t, ch⟩ ∈ pending
pending = pending \ {⟨t, ch⟩}
consumed = consumed ∪ {⟨t, ch⟩}

KV TTL: a pending challenge silently disappears after 300s.

Expire(t, ch) ≜
∧ ⟨t, ch⟩ ∈ pending
pending = pending \ {⟨t, ch⟩}
UNCHANGED consumed
Next
tTenants, chChallenges :
Issue(t, ch) ∨ Verify(t, ch) ∨ Expire(t, ch)
SpecInit ∧ □[Next]vars
TypeOK
pendingPairs
consumedPairs

Single-use: once verified, a pair can never be live again, so a second POST of the same credential response always fails the KV lookup.

NoReplaypendingconsumed = {}

Gratos_Challenge.cfg

SPECIFICATION Spec

Quiescence is legal: with the finite challenge pool, a state where every (tenant, challenge) pair has been consumed has no enabled action. That is the never-repeats encoding running dry, not a stuck protocol.

CHECK_DEADLOCK FALSE
CONSTANTS
Tenants = {t1, t2}
Challenges = {c1, c2}
INVARIANTS
TypeOK
NoReplay

Generations

genchangesdistinct statesdepthpublishedraw
1 (latest) First publication to the hub; previously validated with Java TLC. Config now records CHECK_DEADLOCK FALSE: quiescence when the finite challenge pool is exhausted is legal, matching the CLI flag used in earlier Java runs. 81 9 2026-07-20 17:33:33 UTC .tla .cfg

Defend this spec

Ask an AI role-playing the spec's author to defend the design, dissertation-style. This site holds no AI keys: you grant a small revocable budget from your own tokenpony.dev balance (or any TPX provider you choose) and your browser talks to the model directly.

Loading…