DEOS Capability Revocation — v1 Specification

Status: v1.0 — published 2026-05-02 Spec URI: https://projectmaya.deoscomputing.io/revocation/v1 Maintainer: DEOS Computing (did:web:deos.computing)

A capability token is intended to be valid until its exp time, but real deployments need to revoke a cap before then — keys get compromised, agents get lost, policies change, employees leave. v1 ships an explicit revocation path:

Receipts issued before a revocation remain valid — the capability was authoritative at the moment. This is what makes revocation forensically honest rather than retroactive history-rewriting.

This document is normative for v1.


1. Token hash

The revocation identifier is:

token_hash = lowercase_hex(BLAKE3-256(JCS(token)))

The canonical bytes include the token's s field. Two structurally identical tokens with different signatures have different hashes; revoking one does not revoke the other. This is correct: each issued token is its own delegation, and revoking it cuts that specific branch of authority.

A future v1.1 may introduce broader revocation forms (revoke by (iss, nnc) or by a parent's revocation propagating to children); v1 stays simple.

2. Revocation entry

{
  "schema": "deos_revocation_v1",
  "token_hash": "<64-char hex>",
  "revoked_at_ns": <unix nanoseconds>,
  "reason": "<short human-readable string, optional>",
  "revoked_by": {
    "kid": "did:deos:substrate-alpha-1#sig-1",
    "alg": "ed25519",
    "sig": "<base64url-nopad of 64 bytes>"
  }
}

Fields:

Field Required Notes
schema yes MUST equal "deos_revocation_v1".
token_hash yes The token being revoked.
revoked_at_ns yes The substrate's wall-clock time of revocation, in unix nanoseconds.
reason optional Free-form. e.g. compromised, policy-change, expired.
revoked_by yes Inline signature object.

2.1 Signature payload

sig_payload = "deos-revocation-v1:" || JCS(entry minus { revoked_by })
sig         = base64url-nopad(Ed25519_sign(substrate_secret, sig_payload))

Verifiers MUST verify the signature using the substrate's published kid (from GET /attestation/v1/trust-root) before trusting revoked_at_ns.

3. Revocation log

Revocation entries are appended to a separate MMR under <data-dir>/revocations.bin — distinct from the receipt log (/merkle/log/v1). The leaf data of each MMR entry is the canonical bytes of the revocation entry (with revoked_by populated).

The revocation log root advances monotonically. v1 does not yet expose inclusion proofs or anchor the revocation log to OpenTimestamps; that ships in v1.1 and uses the same machinery as the receipt log.

4. Endpoints

4.1 POST /attestation/v1/revoke

Body:

{
  "token_hash": "<64-char hex>",
  "reason": "<optional>"
}

The substrate:

  1. Builds + signs a revocation entry per §2.
  2. Appends to the revocation log.
  3. Persists the entry to <data-dir>/revocations.json.
  4. Returns:
{
  "token_hash": "...",
  "revoked_at_ns": 1777699183918951000,
  "leaf_index": 0,
  "leaf_count": 1,
  "revocation_root": "<hex>"
}

In v1 the endpoint is unauthenticated. Production deployments MUST gate this endpoint to authorized callers (substrate operator only — most systems should NOT expose it on the public attestation API).

4.2 GET /attestation/v1/revocations

Returns the full set:

{
  "leaf_count": 101,
  "root": "<hex>",
  "entries": [
    {
      "schema": "deos_revocation_v1",
      "token_hash": "...",
      "revoked_at_ns": ...,
      "reason": "...",
      "revoked_by": { ... },
      "_leaf_index": 0
    },
    ...
  ]
}

_leaf_index is an underscore-prefixed informational field added by the server to surface the entry's position in the revocation log; it is not part of the signed body and verifiers MUST ignore it for signature verification.

v1.1 introduces:

4.3 GET /attestation/v1/revocation/{token_hash}

Returns the single signed entry for token_hash if present, or 404 if not.

5. Time-of-revocation semantics (normative)

For each chain token in a receipt:

  1. Compute its hash per §1.
  2. If the hash is NOT in the revocation set: this token contributes nothing to the revocation check. (Capability still valid.)
  3. If the hash IS in the revocation set, with revoked_at_ns = T_r:
    • If receipt.issued_at_ns < T_r: the receipt was issued BEFORE the revocation. The receipt remains VALID. (Pre-revocation issuance is still good — the cap was authoritative at the moment.)
    • If receipt.issued_at_ns >= T_r: the receipt was issued AT or AFTER the revocation. REJECT.

The substrate enforces this at POST /append. The external verifier enforces it at every receipt fetch.

The "issued_at" comparison is sensitive: a malicious substrate could backdate issued_at_ns to evade revocation. The robust mitigation is M5's OpenTimestamps anchor — once the receipt's log root is anchored to Bitcoin, issued_at_ns is bounded above by the anchor's Bitcoin block timestamp. v1 verifiers MAY:

5.1 Offline-verifier tolerance window (REV1.1-10)

A verifier without live access to a substrate's /revocations endpoint operates against a cached snapshot. The cache MAY be stale: tokens revoked after the snapshot was taken will not be reflected. This section specifies the normative behavior for offline / cache-staleness scenarios.

Tolerance window. Verifiers MUST track cache_fetched_at_ns for every revocation snapshot they hold. A receipt is conservatively-valid-offline if and only if:

receipt.issued_at_ns + tolerance_window_ns < cache_fetched_at_ns

That is: the receipt was minted at least tolerance_window before the verifier's last revocation refresh. This guarantees that any revocation submitted at or before receipt.issued_at_ns would have been visible in the cache, modulo the operational propagation delay of tolerance_window.

Default tolerance. v1.1 substrates SHOULD propagate revocations within 5 seconds (tested at M7 acceptance). Verifiers SHOULD use tolerance_window_ns = 30_000_000_000 (30s) for online deployments and 300_000_000_000 (5 min) for offline/edge deployments. Operators MAY tighten via deployment policy.

Receipts within the window. Receipts whose issued_at_ns falls inside [cache_fetched_at_ns - tolerance_window_ns, cache_fetched_at_ns] MUST be either:

  1. Re-verified online before the receipt is acted upon, OR
  2. Held with a "PROVISIONAL — refresh required" annotation if online verification is unavailable.

A verifier that returns OK for an in-window receipt without explicit refresh-or-PROVISIONAL handling is non-conformant.

Receipts after cache_fetched_at_ns. Receipts whose issued_at_ns is GREATER than the verifier's cache time are necessarily un-evaluatable offline: the verifier hasn't seen the revocation log advance to that point yet. These MUST be rejected (or PROVISIONAL) until the verifier refreshes.

Proof artifacts. The verifier MUST include cache_fetched_at_ns and tolerance_window_ns in any reasoning trace it emits for an offline verification. Auditors reviewing offline-verified receipts can then confirm the staleness assumption was within policy.

6.1 Extensions

6.1.1 Revoke by (iss, nnc) (REV1.1-5)

POST /revoke accepts EITHER {"token_hash": "<hex>"} (the v1.0 form) OR {"token_iss": "<did>", "token_nnc": "<string>"} (the v1.1 by-pair form). Both produce a single revocation MMR leaf signed by the substrate.

The by-pair form is for incident response: an issuer that suspects a key compromise can revoke ALL incarnations of a token sharing (iss, nnc) without recomputing each token-hash. The substrate maintains a parallel iss_nnc_revocations index on disk (iss-nnc-revocations.json); chain validation rejects a token if EITHER index hits.

The substrate marks by-pair revocation entries with _form: "by_iss_nnc" and includes the token_iss and token_nnc fields in the entry body so consumers of /revocation/{hash} can tell the two forms apart.

Synthesizing the token_hash for the entry: BLAKE3("deos-iss-nnc-revoke-v1:" || iss || \0 || nnc), truncated to 32 bytes (64 hex chars). Distinct from the per-body token_hash a v1.0 client would compute, and unambiguously the by-pair form.

6.1.2 Cascade revocation (REV1.1-6) [v1.3]

Revoking a parent token in a delegation chain SHOULD revoke all child delegations issued through it. v1 does NOT do this automatically — revocation is per-token. The audit recommended cascade.

v1.3 plan:

Why deferred: needs a per-(parent_token_hash) index that doesn't exist today. Operator-side workaround for v1.1: manually identify child tokens in the audit trail and submit individual /revoke requests.

6.1.3 Time-bound revocation (REV1.1-7) [v1.3]

Today a revocation is effective from revoked_at_ns forward; receipts issued BEFORE that timestamp are not affected. The audit asked for "as of past time T" semantics — invalidate any receipt issued via this token after T, even if T < revoked_at_ns.

This is a security-trade-off feature. Pros: useful for incident response (key leaked at T; want to retroactively invalidate everything signed since). Cons: changes already-validated history, breaks the audit-stable property.

v1.3 plan:

Why deferred: the security trade-off needs explicit operator policy + buyer guidance. Better to leave v1.1 with the simpler "forward-only" semantics until the use case is concrete.

7. Non-membership proofs (MMR1.1-9)

A verifier wants to prove that a token hash X has NOT been revoked as of the substrate's current state. v1 ships the full-set form of this proof; v1.3 will add a sub-linear sparse-Merkle-tree (SMT) form.

7.1 Endpoint

GET /attestation/v1/non-membership/{token_hash} returns:

{
  "token_hash": "<64 lowercase hex>",
  "absent": <bool>,
  "as_of_leaf_count": <u64>,
  "as_of_root": "<hex of revocation_log root>",
  "revoked_set_size": <u64>,
  "revoked_set_sorted": ["<hash1>", "<hash2>", ...],
  "substrate_signature": {"alg": "ed25519", "kid": "...", "sig": "<b64u>"}
}

The substrate_signature covers "deos-non-membership-v1:" || canonical({as_of_leaf_count, as_of_root, revoked_set_sorted}). Domain-separation tag registered in /registry/v1 §6.2.

7.2 Verification (normative)

A verifier given a non-membership proof MUST:

  1. Verify substrate_signature against the substrate's published key for the recorded kid (resolve via /trust-root or /trust-root-history).
  2. Confirm revoked_set_sorted is in lexicographic order; reject otherwise (the substrate cannot omit hashes by claiming an out-of-order set).
  3. Confirm the queried token_hash is NOT a member of revoked_set_sorted.
  4. Cross-check as_of_root against an authoritative source (a witness observation per /mmr/transparency/v1 §3, or a /revocations response captured at the same time).

If all four checks pass, the verifier has cryptographic evidence that, as of as_of_leaf_count, the substrate had not revoked the token. Combine with the offline-tolerance window (§5.1) to convert into a "valid as of T_window" verdict.

7.3 Limitations and v1.3 path

The v1 form ships the entire revoked set in the response. Substrate caps at 5000 entries and returns 503 above that bound. At v1.1 healthcare scale (typical 100-1000 active revocations) this is ~6-65KB per query — fine. At enterprise scale (10000+ revocations) it would be too large.

v1.3 ships an SMT (sparse Merkle tree) form: revoked token-hashes are inserted into a sorted SMT keyed by their hex; non-membership proof consists of two adjacent leaves bracketing the queried hash plus their inclusion proofs. Size is O(log N) regardless of set size. The wire format will be additive-compatible with v1's full-set form: clients consuming v1 keep working; clients capable of v1.3 prefer the SMT form for efficiency.

The substrate's revocations HashMap will be replaced by a SMT under the hood; the /revocations endpoint returns the same shape but with SMT-based pagination.

6. Race window

There is a single-server race window between:

In v1 the server holds a single mutex around all of /append and /revoke. Sequential operations are serialized, so within a single substrate the race window is sub-RPC: an admit decision is made under the same lock that observes the current revocation set. Multi-substrate deployments need consensus (v1.2 / federation work).

7. Open questions deferred


8. Maintainer

DEOS Computing — design questions and conformance reports to github.com/DEOS-Computing.

License: CC BY 4.0 (text), Apache-2.0 (reference code).