rootcard.org
Developers

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.

Relying party

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
Host

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 vault feature.
  • 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());
Client

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 }) });
rootcard.org as a home

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.

EndpointWhatLimit per address
GET /hostHost key, addresses and realms120 / min
GET /id/lookup?realm=global&handle=…Resolve a handle to an account id and its home120 / min
GET /id/{account}/logThe newest log120 / min
GET /id/{account}/publicThe signed public profile120 / min
POST /id/registerRegister an identity with this home3 / hour
POST /id/{account}/eventsNewer events (rotation, retirement)30 / hour
POST /id/{account}/instanceMove here or reset (key tool)30 / hour
GET /id/{account}/locksLock hints for sign-in30 / hour
POST /id/{account}/challengeA single-use challenge (60 s)10 / min
POST /id/{account}/fetchThe vault, after a lock proof30 / hour, 10 / hour per identity
POST /id/{account}/loginSign in; returns a bearer token10 / min
/session/*Vault, profile, password change, export, logout120 / 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.

Packages

Everything is open source

RegistryPackageWhat
crates.iorootcardCore library: formats, verification, vault, proofs. Pure, no I/O
crates.iorootcard-hostHelpers for hosts: challenges, stores, HTTP-agnostic handlers
crates.iorootcard-cliThe key tool as a command (rootcard)
npmrootcardThe same core in plain ES modules, zero dependencies, and keytool.html