Anchored did:qal documents + challenge auth (RFC 9421-shaped) · qalid/README.md
qalid — anchored digital identity v0
Created 2026-09-10. did:qal — a W3C-DID-shaped identity method where the
DID document is signed with the qalchat identity key and its sha256 is
anchored on qalnet via the qalpipe DataAnchor move call. Resolve =
verify the signature and re-verify the anchor against the chain, so a
tampered document is caught in one command.
Part of the qal identity stack: implements the v0 slice of
09_identity_ai/IDENTITY_AND_AI.md (the DID/VC design) over the primitives
from content/identity_stack.md.
┌────────────────────────────────────────────────────────────┐
│ qalid — DID docs, challenge proofs, :8838 resolver/auth │ ← YOU ARE HERE (v0)
├────────────────────────────────────────────────────────────┤
│ qalchat — identity keys (ed25519/x25519), qoll-codes │
│ qalpipe — sha256 → qal_data::anchor (chain) → verify │
│ qalnet — devnet :9000 (DataAnchor objects live here) │
└────────────────────────────────────────────────────────────┘
What this is vs full W3C DID — honest scope
| Have in v0 | Don't have yet |
|---|---|
did:qal:<handle> identifiers |
❌ universal resolver / other DID methods |
DID document (W3C @context, verificationMethod, service) |
❌ JSON-LD context machinery (plain canonical-JSON, not RDF) |
| Ed25519 signature over canonical-JSON of the doc | ❌ key rotation (re-anchor = create --force, new tx) |
| On-chain tamper-evidence (sha256 anchored, PASS/TAMPERED on resolve) | ❌ revocation registries (no BitstringStatusList / RevocationBitmap2022 yet) |
Challenge/response auth primitive (prove / verify-prove) |
❌ capability VCs, spend mandates, Hierarchies delegation |
| HTTP resolver + auth service on :8838 | ❌ Signature-Agent header / Web Bot Auth directory |
x25519 key-agreement key published in the doc (#enc-1) |
❌ encrypting to a DID (qalchat already does E2E — this wires the key into the doc) |
Upgrade path (per the design docs): DID docs → add status-list service
entries; revocation → BitstringStatusList objects anchored the same way
qalpipe anchors everything else (flip a bit, ~seconds, verifiable by anyone
resolving the DID); delegation → Hierarchies-style accreditations issued BY a
did:qal root. The anchoring primitive (qalpipe.anchor_file) is already
generic — every upgrade is "anchor a new object type", not "rebuild the stack".
Keys are multibase-ish (f + hex, multibase base16) — portable to
z+base58btc later with one encoder.
Files
qalid.py— zero-new-dependency CLI + service (stdlib +cryptography, already required by qalchat). Reuses the qalchat identity store — it does NOT mint second keypairs:createreads your existing identity (or creates one, same schema, local-only).test_qalid.sh— 17-assertion live-chain suite (exit 1 on any failure).data/dids/<handle>.json— resolution record{doc, sig, anchor:{tx, object_id, sha256}}.data/dids/<handle>.envelope.json— the EXACT canonical bytes the chain commits to (the anchor can't live inside the anchored bytes — hash bootstrapping — hence record + envelope split).
CLI
python3 qalid.py create alice # sign doc + anchor sha256 → tx printed
python3 qalid.py resolve did:qal:alice # sig check + chain re-verify → PASS/TAMPERED
python3 qalid.py resolve alice # bare handle works too
python3 qalid.py prove alice "nonce-42" # → {did, challenge, sig}
python3 qalid.py verify-prove alice "nonce-42" "<sig>" # verifier needs NO keys
python3 qalid.py serve --port 8838
python3 qalid.py list
Exit codes for resolve: 0 verified · 2 TAMPERED (chain mismatch) ·
3 INVALID signature · 4 not found. verify-prove: 5 bad proof.
JSON on stdout, human summary on stderr — pipes are clean.
Agents: authenticate to a service (the RFC 9421-shaped v0)
No shared API keys — the server resolves your DID, you sign its nonce:
# 1. service issues a challenge (stored server-side, 5-min TTL)
CH=$(curl -s -X POST http://127.0.0.1:8838/prove/alice | python3 -c 'import json,sys;print(json.load(sys.stdin)["challenge"])')
# 2. agent signs with its identity key
SIG=$(python3 qalid.py prove alice "$CH" 2>/dev/null | python3 -c 'import json,sys;print(json.load(sys.stdin)["sig"])')
# 3. service verifies against the CHAIN-VERIFIED doc key
curl -s -X POST http://127.0.0.1:8838/prove/alice/verify \
-d "{\"challenge\":\"$CH\",\"sig\":\"$SIG\"}" # → {"verified": true}
Replay fails (challenge is single-use, TTL-bounded); forgery fails (verifier
checks the signature against the doc's own #keys-1, and the doc against the
chain). The signature covers qalid-prove-v1|<did>|<challenge> — the same
shape as an RFC 9421 HTTP message signature, minus the HTTP header plumbing.
Service endpoints: GET /health, GET /did/<handle> (resolve incl. chain
check; 409 if tampered), POST /prove/<handle>, POST /prove/<handle>/verify.
Integration points
- qalsh — agent shells sign challenges instead of holding bearer tokens; a shell's DID doubles as its audit identity (every action attributable).
- qmarket / qalbazaar — order + settlement records anchored per seller DID;
capability checks (
can-sell, spend caps) are the next credential type. - qalchat — same keys, same qoll-code (
doc.qollCodematches the invite fingerprint), so a DID and a chat identity are provably the same actor.
identity v2 — humans, agents, devices, one system (2026-09-11)
Every DID doc now carries a subjectType (human | agent | device |
service) and a controller DID. The Qal ID design rule: never
self-sovereign agents — humans have controller=self; every agent, device
and service names its human/org DID, and the controller's ed25519 signature
over the doc hash (qalid-control-v1|<did>|<type>|<controller>|<doc-sha256>,
envelope.controller_sig) is REQUIRED at creation and re-verified on every
resolve against the controller's own anchored doc. Unsigned creation is
refused; a stripped/forged controller sig fails resolve (exit 8).
python3 qalid.py create tradegpt --subject-type agent --controller alice
python3 qalid.py device-add alice testlab --type rig # → did:qal:alice--testlab
python3 qalid.py devices alice # controller's registry
python3 qalid.py resolve did:qal:alice--testlab # controller auth checked
Devices connect like tailscale connects machines: device-add derives the
device's ed25519 + x25519 keys from the CONTROLLER's BIP-39 mnemonic via
seedkit qal-derivation v1 (new domains device/<name>, device-enc/<name> —
same seed + same name ⇒ same keys), builds a device DID
(subjectType=device, controller=<parent>, non-secret derived provenance),
anchors it, and hands back the device's qoll-code plus its own tiny signing
key (a qalchat-schema identity file, 0600). The device then authenticates
itself with the same prove / verify-prove as any human.
Tailscale bridge (network layer ⇄ value layer):
python3 qalid.py tailscale-bind did:qal:alice--testlab # or: alice--testlab / testlab
python3 qalid.py tailscale-audit alice
tailscale-bind runs tailscale status --json (subprocess JSON — zero new
deps; tests mock it with env TS_OVERRIDE_JSON={json|path}), matches the node
by hostname, and records {hostname, dnsName, nodeKey, nodeKeyHash, online,
checkedAt} into the doc's service[] (type TailscaleNode), then
re-signs (device + controller) and re-anchors. tailscale-audit cross-checks
every bound device against live tailscale state: ONLINE / OFFLINE (node
key matches), KEY-MISMATCH (live node key ≠ DID record — substitution
alarm, exit 1), UNKNOWN-IN-TAILSCALE (bound host missing, exit 1).
Revocation + recovery:
python3 qalid.py revoke did:qal:alice--testlab --reason "lost"
python3 qalid.py recover-plan alice # per-type story, exact commands
revoke appends a controller-signed revocation entry
(qalid-revoke-v1|<did>|<at>|<by>), re-anchors the envelope, and every
resolver then reports REVOKED (exit 6) and refuses proofs (exit 7).
recover-plan prints the contract per type: human → the mnemonic
restores identity + agents + devices (all derived); seed loss is catastrophic
— Stronghold vault + ≥2 offline backups (see 09_identity_ai/IDENTITY_AND_AI.md
§key custody); agent/service → controller-recoverable by design (re-issue
a fresh DID, revoke the old); device → stolen ⇒ revoke, never re-derive
(the thief holds the key); wiped ⇒ same mnemonic + same name re-derives the
same keys. New exit codes: 6 REVOKED · 7 proof refused (revoked) ·
8 controller authorization invalid. v0 docs (no subjectType) still
resolve — humans, controller=self.
Run
Chain must be up (qalnet/qal/README.md; it stores the DataAnchor objects).
Relay :8830 / loopd :8823 are referenced in docs but not required by qalid.
bash test_qalid.sh # full proof-of-life, live chain