# Experimental device identity foundation (DH-004) Status: experimental library, integrated into the explicitly enabled **public** snapshot transport; private remote authentication remains disabled. Uses Go standard-library Ed25519/SHA-256. No wallet signature schemes, custom crypto primitives or secrets over HTTP. `internal/identity` is deliberately separate from discovery publisher IDs and the DOP author's claim. An explicit, reviewed adapter is required before these identity families interoperate. ## Encoding and contexts The signed message is UTF-8 `context + NUL` followed by Go `encoding/json.Marshal` of the **declared typed struct**, in its declared field order, without whitespace. This is not arbitrary-JSON canonicalization/JCS. A signature field is present as an empty string when signing Delegation or Revocation. Arrays preserve order; duplicate/unknown permission entries are rejected. Binary public keys/signatures/nonces use canonical unpadded base64url. Hashes use lowercase 64-digit hex. Unknown schemas are rejected. Current structures and field order live in `identity.go`; format changes require a new version. Contexts: - Identity ID: `dh:id:` + SHA-256(`decentralhub/identity/id/v1` + NUL + 32 public-key bytes). - Device delegation signature: `decentralhub/identity/delegation/v1`. - Delegation ID: SHA-256(`decentralhub/identity/delegation-id/v1` + NUL + complete signed delegation JSON). - Revocation signature: `decentralhub/identity/revocation/v1`. - Request proof signature: `decentralhub/identity/device-proof/v1`, over `{challenge, delegation_id}` in that order. Fixed public-key vector: `03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8` → `dh:id:4f4f80b0f2d83615fcf0c794734580ddd102b3a60f4235394cf3bd5eabfbdf16`. Verified by a test and independently computed with Python hashlib. ## Delegation and deny-by-default A root key signs a device key, one exact repository scope, an explicit list of permissions, issue time and expiry. Supported permissions are `read`, `propose`, `review`, `write`; none implies shell execution or another permission. No wildcard repository scope. Lifetime is at most 30 days. A device proof does not prove legal identity, repository ownership or wallet ownership. The integrating server must select the owner from a trusted ACL, derive the required permission from the operation, and supply the exact request body and target. A caller-selected owner key/permission is never an ACL. Revocation is signed by the same root and applies to one delegation ID. Keys with a cached public half inconsistent with their seed are rejected. Master keys are never sent to a node by this API; clients sign locally. OS-backed keystores, root rotation, recovery, persistent device inventories and cross-node revocation propagation remain unimplemented. ## Bound challenge A server authority is configured with a trusted owner public key, origin and audience. It issues a cryptographically random 32-byte nonce and records exact repo, required permission, method, target, body SHA-256, identity, origin, audience, issue time and a five-minute expiry. HTTPS origins are required except explicit loopback HTTP. The client must supply its separately trusted expected origin/audience to `Respond`; it must not merely echo a server's claims. Request bodies are limited to 1 MiB; targets to 2048 bytes. Authorization rechecks root signature, scope, permission, expiry, revocation, request binding and device proof; the nonce is consumed under a mutex only after successful verification. Concurrent replay has exactly one success. Authorities hold at most 1024 live challenges and 4096 revocation records. Restart invalidates challenges. `NewAuthority` is explicitly ephemeral; persistent authorities use the separate initialization/opening APIs below. This library does not activate private remote repositories on its own. System-clock correctness is an operating assumption; secure time/rollback policy is still open. ## Persistent authorities and trust boundary `CreateAuthority(directory, trustedOwnerPublicKey, origin, audience)` initializes exactly one new `revocations.json` in a pre-existing trusted directory and refuses an existing file. `OpenAuthority(...)` requires existing valid state: missing, malformed, oversized, symlinked or wrong-owner state is an error, never an implicit reset. `Close` invalidates nonces and releases directory handles. Neither API receives or writes an owner private key. The public owner key must come from a trusted local ACL/configuration, never from an untrusted incoming request. Persistent schema: `dh.authority-revocations/1`, followed by `identity` and `revocations`, in that exact typed JSON field order. Revocations are sorted by delegation ID, unique, individually signed and verified against the configured owner. Unknown fields, duplicate/noncanonical JSON, invalid signatures and counts over 4096 fail closed. Maximum state file is 2 MiB. A valid revoked delegation cannot be unrevoked through this API; duplicate applications are idempotent. The generic `internal/state` store uses a pinned directory, one named JSON file, bounded regular-file reads, an OS process lock, unique temporary file, file fsync, atomic replace and Linux directory fsync. Views and updates acquire the same lock; updates re-read existing state, so two open instances/processes cannot overwrite each other's revocations. Every Issue and Authorize reloads and validates current state, including when a different process applied the revocation. A post-install I/O failure reports an uncertain result; authorization still reloads storage and never falls back to an old allowed-rights cache. The state file is not a database of private keys. Windows file contents are flushed, but directory power-loss durability remains an explicit platform gate. Authorization's decision linearizes while it holds the revocation-store lock. A revocation completed before that decision prevents authorization; a write already authorized before revocation may finish. This is not a promise to cancel in-flight operations. A transport must independently bind authorization to its actual operation/ACL and protect atomic reference updates. Challenges remain in the issuing instance only. A restart or a different instance rejects a proof for an unknown nonce, which is safe but requires clients to obtain a fresh challenge from that instance. No restoring/reaccepting old nonce database occurs. Persistent authority state does not itself provide load-balancer routing. The filesystem directory and its administrator are trusted. Individual signatures prevent forging a revocation; they do **not** prove that a privileged local actor has not rolled back the entire file or omitted a signed record. Detecting such a rollback across machines requires a separately trusted monotonic checkpoint or audited replication architecture. Deleting state is detected; intentionally reinitializing a different authority directory is an administrative trust change, not recovery. Never delete the `.lock` file while instances are running. ## Tests and remaining gates Fixture tests cover inconsistent private keys, identity hashing, altered certificates, unknown/duplicate permissions, expiry, wrong owner, signed/forged revocation, origin/audience mismatch, wrong repo/method/path/body/permission, nonce reuse and concurrent replay. Additional persistent-state tests cover two open authorities, separate-process revocation, reopening, deleted/corrupt/wrong-owner/forged state, restart nonce denial, multi-instance serialized state updates, and five process-crash/I/O-failure boundaries. No live user keys or projects are used. The public transport now binds HTTP operations to its persistent configured owner and current signed revocations. OS keystores, group ACLs, recovery/rotation, production TLS deployment and private E2EE remain separate gates. See REMOTE-SYNC-DRAFT.md for the tested integration and its limits.