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:
- the nostr binding (k2), only if the person said yes: a third verification method
#nostr(Multikey,publicKeyMultibaseof multicodece701plus the compressed key,0x02then the x only key), listed underassertionMethodonly;alsoKnownAs: ["nostr:npub1…"]; andbindings: [{ "key": "#nostr", "signs": "#sign", "scheme": "bip340", "signature": "<hex>" }], the nostr key's bip340 signature oversha256(raw bytes of the root signing key), so the link verifies both ways offline (vector:vectors/nostr-binding.json). - a rotation (k6): a second entry in
proofby the previous key,proofPurpose: assertionMethod, itsverificationMethodthe previous key named offline asdid:key:<previous multikey>#<previous multikey>, andpreviousCardset to the previous card'sid. (previousProofis a data integrity chain term with another meaning, so OHAN does not use it.) the new card'stsmust be newer than the previous card's. - an agent (c4):
controllerset to the root's did and a second entry inproofby the root's current key withproofPurpose: capabilityDelegation. - an application's endpoint:
endpoint, an https url; the did document then lists it as service#gateway(OHAN-14 §7). profile.claimsandprofile.vouchesmust be empty arrays or absent in v1. other members are reserved (OHAN-14 §13).
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
GET /<handle>/did.json(and/<root>/<agent>/did.json): a did document with the two (or three) verification methods, the relationships (authentication,assertionMethod,capabilityDelegation,capabilityInvocationon#sign;keyAgreementon#enc;assertionMethodonly on#nostr),controlleron an agent,alsoKnownAs, and services:#card(the url of the card),#relays(OhanRelay, the relay urls),#gatewayifendpointis set.#statusis reserved. noappsservice. a deactivated handle (c9) servesdeactivated: truein the metadata.GET /@handle: the public page: the handle, the chosen profile fields, the fingerprint (eight groups of the signing key), "add @handle with openthings", links to the card and the did document. nothing else.- the compact pin clients keep:
{ handle, did, sign, enc, relays, card: <hash> }, wherecardis the sha256 (hex) of the jcs of the whole card, proof included. the same hash travels assender.cardin every envelope.
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.