An unknown browser pays once, mints a key it never gives up, and is handed a seven-day certificate. After that it proves possession, not humanity. The server gets something it can rate-limit, allow, deny and revoke: a recurring identity with no username, no password and no third party.
Runs in this page: a private CA (ECDSA P-256), a challenge store, an allowlist and denylist, and a session signer. Its state persists in IndexedDB so a return visit is real.
Server issues a 16-byte challenge with a difficulty. Browser finds a nonce with SHA-256(challenge ":" nonce) starting with that many hex zeros. Single use, two-minute TTL.
WebCrypto generates an ECDSA P-256 key pair; the private key is non-extractable and never leaves the browser. The public key (SPKI) goes into a signing request that the browser signs with the new key.
The CA checks the proof of work and the request's self-signature, then signs a certificate: serial, subject, SPKI, issuer, seven days of validity. The fingerprint is SHA-256 of the SPKI.
Server issues a fresh challenge; browser signs it with the private key. Server verifies the signature against the certificate, the certificate against the CA, the validity window, and the fingerprint against the lists. Then it mints a session token.
No certificate yet.
Issued after a proof of possession. HMAC-SHA-256 over a JSON payload, the way the original JWT sessions were.
This is the four-stage enrollment from the Rust and Flask experiments, with the server folded into the page so the whole protocol can be inspected. The original started with RSA-2048 and moved to EC P-256; this build is P-256 only. The certificate here is a signed JSON document rather than X.509, because the browser cannot read X.509 without a library and the protocol does not need it; the fields and the trust chain are the same.
Nothing here stops a bot from enrolling; it stops a bot from enrolling for free and from pretending to be many browsers without paying many times. The lever is what the server does with the fingerprint afterwards: per-identity rate limits, denylist on misbehaviour, and the fleet statistics from the gates in front of it.