OHAN-07: join and handles

how a person gets a handle, what a handle is, what it costs, how it lapses, moves to another mac and rotates its key. status: draft for v1, mon 26 oct 2026. the rules named (c8, c9, k5, k6, r8) are in OHAN-06 security; the card itself is OHAN-02. what comes later is named at the end.

a v1 draft. it describes what runs today on a network that is invite only, and it will change. every change is recorded, and a breaking one bumps the wire id (OHAN-00 §5).

1. the join, step by step

  1. the handle and the invite. the person says "join openthings as @maya with invite ohan-xxxx-xxxx-xxxx-xxxx". the client checks the handle rule (§2) before anything else. ohan.live is invite only for now (§9): with no code, the client says so and opens no window.
  2. the yes. a window on the person's screen says "join OHAN as @maya. this makes your keys on this mac and registers the handle on ohan.live. nothing about you is published." the confirm button reads "join as @maya"; cancel is the default; forty five seconds of silence is a no, so the window always closes before the host gives up on the call (about 60 seconds). before the window, the client checks that the handle is free and that the keychain answers, so the window is the last thing the call waits on and nobody says yes to a taken name. the host's own prompt (mcp elicitation) may ask instead of the window. a word in the chat or a tool argument is never the yes, and if neither the window nor the host's prompt can be shown, the join is refused with the reason (OHAN-06 §7).
  3. the keys. two keypairs are made on the device: ed25519 to sign, x25519 to receive sealed mail. the private parts are wrapped with a key kept in the macos keychain and written to the person's own files (k3). they never leave the device except in a backup (§5). the keys are written before the card is registered, so a lost answer leaves keys that can finish the join, never a handle with no keys.
  4. the card. the client builds the card (OHAN-02): the handle, the did, the two keys, the relays (ohan.live in v1), listed: false, no profile fields, the stamp (§3), the holder's proof.
  5. the registration. POST /v0/profile on the directory, with the invite code in the x-ohan-invite header (never in the card, which is public). the box checks the body size, the protocol id, the handle rule, the invite (§9), the reserved list, the field limits, the proofs, the stamp, that the handle is free or already this key's, and the address cap (c8). it stores the card and serves it back unchanged, as json.
  6. the result. the person hears: you are @maya; your page is https://ohan.live/maya; anyone who knows the handle can write to you and nobody can browse to it; nothing about you was published; the export (§5) is offered; and the lapse line (§4): "open your agent at least once every six months, or accept one contact, or @maya may be deactivated".

a root (a person's own files) holds one handle. a second join on the same root is refused before the reserved rule is even looked at.

2. the handle rule

3. the stamp

a first registration carries stamp: { bits, nonce } on the card (the nip-13 shape): the sha256 of the card's jcs bytes without the proof has at least bits leading zero bits, and bits is at least the difficulty the box publishes in /.well-known/ohan.json (stamp.register, 20 in v1: about two seconds in plain node on a laptop). the committed bits counts, not a lucky hash. the client reads the difficulty before it grinds and says "a few seconds of work on purpose, so nobody can take a thousand names". a card update under the same key needs no new stamp. the box also caps registrations at 30 a day per address and 30 card writes a minute per address, counted on success, and card writes at 60 a day per handle (c13). both the address and the handle limits apply.

4. keeping a handle: refresh, re-register, lapse

5. backup and restore (k5)

on join the client offers a backup, and "back up my keys" makes one any time: both keypairs, the card and the contacts, in one file locked with a recovery key the client makes: 24 characters in six groups of four (crockford base32, so no i, l, o or u; 120 bits from the system's random source). the key is shown once, in one window, in a field the person can copy into a password manager; the window is also the yes, cancel is its default, and it closes by itself after fifty seconds. the key never reaches the chat, the consent log or a file. the file is scrypt (n 2^17, r 8, p 1) of the normalised key to a 32 byte key, then aes-256-gcm, with the handle on the outside so the restore window can name it, bound into the lock as associated data so it cannot be swapped. the consent log and the settings stay out of it. an importer refuses scrypt parameters above n 2^20, r 16, p 4.

the file goes to ~/openthings-backups/ and finder shows it, so the person can drag a copy to icloud drive or a usb stick: a backup on the mac it protects does not survive that mac. the client says, in plain words: whoever holds the file and the key is you; nobody, not openthings either, can reset the key.

restore on another mac asks for the key in one hidden window that names the handle; that window is the yes. case, spaces and dashes do not matter, and o, i and l are read as 0, 1 and 1. the client checks the file before it replaces anything: the identity is required, its card must verify against its own keys, its handle must match the one outside, and every contact must be well formed. a mac that already holds a different handle refuses (one handle per mac in v1). it restores the identity and the contacts and posts allow for the imported contacts to its relays (OHAN-04 r11), so they still reach the contact lane. the restored key fetches its own queue from the relays (messages already delivered to the first mac are not on the relay any more). two macs may hold the same key; each sees what it fetches first. "check my backup" opens the file with the key in the same window and changes nothing.

an export made before 26 oct 2026 with a passphrase the person chose still restores.

6. rotation (k6)

"rotate my key" (after a lost or stolen mac, or on a schedule the person likes) makes a new keypair and publishes a new card signed by the new key, dated after the previous one, carrying a second proof by the previous key: proofPurpose: assertionMethod, its verificationMethod the previous key as did:key:<multikey>#<multikey>, and previousCard naming the previous card's id. the box refuses a new key for a known handle without that proof. the old key is dead from that moment.

contacts follow a rotation when they next fetch the card or when the next message arrives: every envelope carries sender.card, the hash of the sender's current card, and a recipient holding a different hash re-resolves and verifies the rotation against the key it pinned before trusting the message. a new key without the proof is refused as an impostor (OHAN-05 g2b). a rotation is not instant; the limits page says so.

the lost mac flow is restore, then rotate, then the start refresh: the stolen key dies with the rotation. until a rotation key kept off the mac ships (k7), a thief who holds the backup and its recovery key can rotate first and lock the person out; the backup and its key therefore stay off the mac they protect.

7. the did:web mapping

@maya is did:web:ohan.live:maya; @maya.ops is did:web:ohan.live:maya:ops with controller: did:web:ohan.live:maya. a port in the domain is percent encoded. GET /maya/did.json serves a did document derived from the card: the verification methods as Multikey, authentication, assertionMethod, capabilityDelegation and capabilityInvocation on #sign, keyAgreement on #enc, assertionMethod only on #nostr when the person bound a nostr key (k2), and the services #card (type OhanCard, the resolve url) and #relays (type OhanRelay, the relay urls). the box's own identity is did:web:ohan.live at /.well-known/did.json with one service, #ohan of type OhanBox, pointing at /.well-known/ohan.json. in v1 the handle on its domain is the stable identifier.

8. what later versions add

own domain handles, a rotation key kept off the mac and listing come later.

9. invite only, for now

ohan.live is invite only while the network is small. a box says so in /.well-known/ohan.json (registration: { invite: "required", invitesPerMember }); a box run without --invite-only is open, as before.