A person is their keys
There's no user database at wellknown.id. A person is known to your site by a key: one did:key per site, so no two sites can match their records of a person by key.
- Per-site keys. A site's key is derived from the person's seed (32 random bytes made at their first sign-in) with HKDF-SHA-256, salt
wellknown.id/site-key/1and the site'sclient_idas info, mapped to a P-256 key. The same seed gives the same key for your site on every device that holds it, and a different key at every other site. - Two kinds of key. The IdP accepts P-256 (
did:key:zDn…, ES256) and Ed25519 (did:key:z6Mk…, EdDSA). The sign-in pages and kivi make P-256 keys; a key kept in a phone's secure hardware would also be accepted. - The seed is the person's. It's reached through any of several unlocks: a passkey (with the WebAuthn PRF extension), a printed recovery code, kivi on their phone, or the people they chose to hold shares of a recovery key. A passkey provider is one way back to it, never the key itself.
Personas
A person may have several seeds, each a persona: work and home, say. Each gives its own per-site keys, so two personas are two different did:keys at your site, and neither your site nor wellknown.id can tell they belong together. One passkey opens all of a person's personas (their keyring); a recovery code opens one.
Which persona signs in is chosen on the person's device: the one used at your site before, the person's choice where several were, else their default. Only the person joins two at your site, with a succession statement or linkKey() (getting started).
A sign-in: a self-issued proof, bridged into OpenID Connect
- Your page (through the web SDK) or your own OpenID Connect client sends the browser to
https://wellknown.id/authwith yourclient_id, aredirect_urifrom your config, anonceand a PKCE challenge, or asks the browser's FedCM dialog. - wellknown.id checks your config (the
redirect_urimust be listed) and gives the sign-in page a single-use challenge. - In the person's browser, wellknown.id's page unlocks their keyring, chooses the persona and signs a self-issued token with their key for your site: SIOPv2's self-issued ID token shape,
typ: wellknown-self-issued+jwt,iss=sub= thedid:key,audyourclient_id,noncethe challenge, alive for 120 seconds. - The IdP checks it in memory (the signature against the
did:key, the challenge, your config and your karu policy) and issues an authorization code, which is redeemed at/tokenfor an ID token it signs with its published keys. The ID token'ssubis thedid:key.
A site whose config allows it may instead take the self-issued token straight from the page and verify it itself: then wellknown.id is sent nothing naming the person's key (the web SDK).
kivi, the wallet app, can sign the same proof for another device: the sign-in page shows a code, kivi on the person's phone scans it, and their keys stay on the phone.
FedCM
In browsers that have FedCM, the SDK uses the browser's own sign-in dialog. wellknown.id lists one generic account, the same for everyone and read from no cookie, and always answers with its own small window, where the proof is made as above. So every FedCM sign-in is a click and a window: there's no silent sign-in, because one could only work by remembering which key a browser used at which site, and wellknown.id remembers nothing of the kind. Each used site's key is cached in the browser, non-extractable, for 12 hours, so the person isn't asked for their passkey again in that time.
No data to leak
wellknown.id is a full OpenID Connect provider, which makes it a target. The design's aim is that there's nothing about anyone to take:
- No accounts, no logs of who went where. The server never keeps or logs keys, seeds, persona names, which key goes with which site, or tokens. What a sign-in needs, it processes in memory for that request and drops: a
did:keylives in memory only in an authorization code and its grant, for the code's 60 seconds at most. - No cookie names a key or a site. There's no session cookie. The only cookies are oidc-provider's two for an authorization in progress, holding a random request id, gone in ten minutes at most.
- What it stores, it can't read. Holders' keyrings, records and the messages their devices leave each other are kept end to end encrypted, under ids nobody can link to a person or to each other.
- The tests check it. Every run scans the IdP's logs, memory, what it was sent and the files it wrote, and fails if any holds a
did:key, a token or a cookie.
What's left to trust is the code the server serves, since wellknown.id's pages run in the person's browser. That's why code integrity is the focus: reproducible builds, a signed public log of releases, Subresource Integrity, and kivi checking the pages against the log (checking what wellknown.id serves).