Two libraries and one HTTP binding.
The core is pure: no I/O, no clock, no randomness of its own. Rust (rootcard,
rootcard-host) and JavaScript (rootcard on npm) produce the same bytes, checked against
the shared vectors of the specification.
Verify users
Any server can check "is this really Astra#3F9C, now?"
with a fresh challenge and verifyProof. No central API.
Run your own host
Give your service a standard username-and-password
login with rootcard-host. No password hashes in your database.
Use rootcard.org
One public home for the global realm, with
the recommended HTTP binding and open CORS.
Verify a proof
Give the user a fresh 32-byte challenge and your own recipient string. The user signs
it with their account key (in their home's client) and sends you the proof and their log. Fetch the newest log
from their home if you want to be sure it is current, then verify. Store at most the newest log per identity
you deal with, and key your accounts by the account id: it never changes.
Signed-in users of rootcard.org can make proofs on their account page.
import { verifyProof, compareLogs } from "rootcard"; // 1. challenge = crypto.getRandomValues(new Uint8Array(32)) // 2. the user returns { proof, log } // 3. optionally fetch the newest log from the home: // GET https://rootcard.org/v0/id/{account}/log const state = await verifyProof(proof, log, challenge, "https://shop.example", { realms: ["global"], now: Math.floor(Date.now() / 1000) }); state.account // the stable key for your user table state.handle // "Astra#3F9C", for display
Run your own home
rootcard-host is a library, not a server: it owns the rules and leaves HTTP,
storage and secrets to you. Implement three storage traits (identities with compare-and-swap, single-use
challenges, sessions), pass your clock and random bytes, and wire http::route into any HTTP
server.
This site is exactly that: an axum server with SQLite storage. Its source is a good starting point.
- Hosts never see a password and never run Argon2id.
- Build the core without its
vaultfeature. - Serve your client pages with a strict CSP; they handle passwords.
# Cargo.toml rootcard-host = "0.3.1" rootcard = { version = "0.3.1", default-features = false } // main.rs use rootcard_host::{Host, HostConfig, RealmConfig, UsernamePolicy}; use rootcard_host::http::{route, Request}; let host = Host::new(HostConfig { key_seed, // your host key retired_keys: vec![], urls: vec!["https://id.example.com".into()], realms: vec![RealmConfig { realm: "myapp".into(), policy: UsernamePolicy::DEFAULT }], peers: vec![], }, identities, challenges, sessions); // for every request under /v0: let res = route(&host, &Request { method, path, bearer, peer, body }, now, &mut |b| getrandom::fill(b).unwrap());
Register and sign in from the browser
The npm package has zero dependencies and no build step. rootcard is the
verification core; rootcard/vault adds passwords, Argon2id and vault encryption. The flows are in
the host protocol, §7. This site's pages are a complete client: registration, sign-in, profile and private
data, password change and proofs.
Generate the recovery key in the client, make the user save the recovery card before you send the registration, then erase it. Never ask for the recovery key in an application client.
import { register } from "rootcard/vault"; import { systemRandom, newRecoveryKey, KDF_DEFAULT } from "rootcard"; const base = "https://rootcard.org"; const d = await (await fetch(`${base}/v0/host`)).json(); const rk = await newRecoveryKey("global", "Astra", systemRandom); const reg = await register(rk, "global", "Astra", password, "Password", { key: d.key, url: base }, Math.floor(Date.now() / 1000), KDF_DEFAULT, systemRandom); rk.fill(0); // show and save reg.card first, then: await fetch(`${base}/v0/id/register`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ log: reg.log, vault: reg.vault, public: reg.public }) });
This home's API
There is no central rootcard API: every home serves the same binding at its own base URL. rootcard.org serves
it for the global realm. Every endpoint below is
under https://rootcard.org/v0. The base URL that goes into logs and signed messages is
https://rootcard.org, without the version.
| Endpoint | What | Limit per address |
|---|---|---|
GET /host | Host key, addresses and realms | 120 / min |
GET /id/lookup?realm=global&handle=… | Resolve a handle to an account id and its home | 120 / min |
GET /id/{account}/log | The newest log | 120 / min |
GET /id/{account}/public | The signed public profile | 120 / min |
POST /id/register | Register an identity with this home | 3 / hour |
POST /id/{account}/events | Newer events (rotation, retirement) | 30 / hour |
POST /id/{account}/instance | Move here or reset (key tool) | 30 / hour |
GET /id/{account}/locks | Lock hints for sign-in | 30 / hour |
POST /id/{account}/challenge | A single-use challenge (60 s) | 10 / min |
POST /id/{account}/fetch | The vault, after a lock proof | 30 / hour, 10 / hour per identity |
POST /id/{account}/login | Sign in; returns a bearer token | 10 / min |
/session/* | Vault, profile, password change, export, logout | 120 / min |
Errors are JSON objects with the specification's codes ({ "error": "not_home", … }).
CORS is open and no endpoint uses cookies. Full details: HOST_PROTOCOL.md.
Everything is open source
| Registry | Package | What |
|---|---|---|
| crates.io | rootcard | Core library: formats, verification, vault, proofs. Pure, no I/O |
| crates.io | rootcard-host | Helpers for hosts: challenges, stores, HTTP-agnostic handlers |
| crates.io | rootcard-cli | The key tool as a command (rootcard) |
| npm | rootcard | The same core in plain ES modules, zero dependencies, and keytool.html |