OHAN-02: the card

the signed public record of a handle. wire id ohan.card.1. status: draft for v1, mon 26 oct 2026; supersedes the demo's card.v0, which a v1 box does not speak. rules k, c and e are in OHAN-06 security.

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 document

{
  "@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/multikey/v1", "https://ohan.live/ns/v1"],
  "protocol": "ohan.card.1",
  "type": "OhanCard",
  "id": "urn:uuid:5f1c…",                         // this version of the card
  "handle": "@anshuli",                            // @ + [a-z0-9][a-z0-9-]{0,30}, an agent adds one .suffix of [a-z0-9-]{1,20}
  "did": "did:web:ohan.live:anshuli",
  "controller": "did:web:ohan.live:anshuli",       // on an agent card: the root's did. absent on a root card
  "verificationMethod": [
    { "id": "did:web:ohan.live:anshuli#sign", "type": "Multikey", "controller": "did:web:ohan.live:anshuli", "publicKeyMultibase": "z6Mk…" },
    { "id": "did:web:ohan.live:anshuli#enc",  "type": "Multikey", "controller": "did:web:ohan.live:anshuli", "publicKeyMultibase": "z6LS…" }
  ],
  "relays": ["https://ohan.live"],                 // up to 8 https urls (http only for a box on this machine). urls and nothing else: no field names a transport
  "accepts": ["message"],                           // intents the gate will consider. optional
  "apps": ["openthings"],                           // short slugs. optional. a card field, never a did service
  "listed": false,                                  // resolvable always; listed only when true
  "profile": {},                                    // each field a separate yes: display_name (80), line (140), links (3 https urls); claims and vouches reserved
  "stamp": { "bits": 20, "nonce": "…" },            // proof of work over the jcs of the card without proof (c8)
  "ts": "2026-10-27T06:00:00Z",                     // must be newer than the stored card
  "proof": [
    { "type": "DataIntegrityProof", "cryptosuite": "eddsa-jcs-2022", "created": "2026-10-27T06:00:00Z",
      "verificationMethod": "did:web:ohan.live:anshuli#sign", "proofPurpose": "assertionMethod", "proofValue": "z…" }
  ]
}

the @context names https://ohan.live/ns/v1, and the box serves that json-ld context there (application/ld+json). it is frozen: a term in it is never redefined, so what a signed card means never changes.

optional blocks:

2. the proof

eddsa-jcs-2022 as data integrity 1.0 defines it: the signed bytes are sha256(jcs(proof options)) followed by sha256(jcs(card without proof)), where the proof options are the proof entry without proofValue; the signature is ed25519; proofValue is multibase base58btc. a verifier MUST resolve verificationMethod to the #sign entry inside the card, never over the network. numbers in a card are integers only. the card's @context is carried so a json-ld processor keeps the OHAN terms; a plain json verifier ignores it, which the cryptosuite allows. proof.created is an iso date time in utc, as is ts. the verification methods are exactly #sign and #enc, with #nostr optional, each once, each of type Multikey and controlled by the card's did; a verifier refuses anything else. a verifier that knows which domain it expects (the box's own, or the domain a handle was resolved from) MUST check did is the handle under that domain.

3. the rules the box applies on write (c2)

in order: the body is under 64 kb; protocol is ohan.card.1; the handle rule; not reserved; the field limits (§1); every proof verifies (the holder's, the rotation's if the key changed, the root's if the handle is an agent); the stamp meets the published difficulty; ts is newer than the stored card; the address cap (30 a day). the box stores the card and serves it back unchanged, as json, from GET /v0/resolve/@handle, whose answer is { "card": … }. the proof is over the jcs of the card, so the json's spacing does not matter.

4. what the box derives

5. the handle and the did

@anshuli is did:web:ohan.live:anshuli; @anshuli.ops is did:web:ohan.live:anshuli:ops with controller: did:web:ohan.live:anshuli. a port in the domain is percent encoded in the did. v1 depth is one suffix. in v1 the handle on its domain is the stable identifier. a handle is never reassigned (c9).

6. what changed from card.v0

keys from hex to Multikey; signature to a data integrity proof set; rotation.signature to a second proof by the previous key (named as did:key), with previousCard naming the previous card's id; the agent countersignature added as a capabilityDelegation proof with controller; id, type, @context added; profile.proofs renamed claims (reserved) and bio, open_to dropped for line, links; the stamp added; apps moved off the did services; the nostr binding added as an opt in block with a cross signature.