Browser Certificate
AdversarialAgents.org · browser certificate gate · browser-cert/1

Browser Certificate

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.

The "server"

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.

1

Proof of work waiting

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.

2

Browser identity waiting

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.

3

Issuance waiting

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.

4

Proof of possession waiting

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.

Policy

deny list only
Allowlist empty
Denylist empty

Server console

no session

Certificate

none
No certificate yet.

Session token

Issued after a proof of possession. HMAC-SHA-256 over a JSON payload, the way the original JWT sessions were.

What it is, and what it is not

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.

  1. The work is paid once. Enrollment costs a proof of work; every later visit costs one signature. Automation that enrolls pays the same price, so the cost is per identity, not per request. That is the point: the server now has a handle.
  2. The key never moves. Non-extractable CryptoKey in IndexedDB. A scraper that copies the page's storage to another machine gets a certificate it cannot use.
  3. The server decides. Fingerprint allowlist and denylist, revocation, seven-day expiry, and the session token's own lifetime. Tick "enforce the allowlist" and the gate becomes invite-only without changing the protocol.
  4. It does not prove a person. A browser with a certificate is a browser that enrolled. Human verification is a separate layer, which is why the hub treats "client authentication" and "human verification" as different columns. The proposed next step was mutual TLS: the same key, the same CA, enforced at the proxy before the application sees a byte.

What beats it

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.