QALS Wiki · the qalarc network⌂ qalarc.com/projects/qals
generated 2026-09-16 · qalcode autonomous research

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

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

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