DEOS Attestation API — v1 Specification
Status: v1.0 — published 2026-05-02
Spec URI: https://projectmaya.deoscomputing.io/attestation/api/v1
Maintainer: DEOS Computing (did:web:deos.computing)
Reference impl: Rust HTTP server in DEOS Computing's working repo (access-controlled during v1 alpha).
Reference verifier: standalone Rust binary, intentionally shares no crate with the server, in the same repo.
The Attestation API is the substrate's outward-facing HTTP surface. Auditors, counterparties, regulators, courts, and SDK verifiers use it to retrieve receipts, capability chains, inclusion proofs, the log root, and the substrate's signing key — everything needed to verify a receipt without trusting the substrate beyond the published trust-root key.
This document is normative for v1.
1. Routes
All routes are under /attestation/v1/. All responses are
Content-Type: application/json. All hashes are lowercase hex (BLAKE3-256,
64 chars). All keys/signatures are base64url-nopad.
| Method | Path | Purpose |
|---|---|---|
| GET | /attestation/v1/health |
Liveness probe |
| GET | /attestation/v1/trust-root |
Substrate's signing identity |
| GET | /attestation/v1/log |
Current log state (leaf_count, root) |
| GET | /attestation/v1/receipt/{id} |
A signed receipt + its log position |
| GET | /attestation/v1/proof/{id} |
An inclusion proof for that receipt |
| POST | /attestation/v1/append |
Submit a receipt for signing + inclusion |
1.1 GET /attestation/v1/health
{ "status": "ok", "service": "deos-attestation-v1" }
1.2 GET /attestation/v1/trust-root
Returns the substrate's issuer DID, key id (kid), algorithm, and public key.
{
"issuer_did": "did:deos:substrate-alpha-1",
"kid": "did:deos:substrate-alpha-1#sig-1",
"alg": "ed25519",
"public_key_b64u": "<base64url-nopad of 32 bytes>"
}
A verifier MUST fetch this once per session (or cache it with a known
expiry) and use it as the trust anchor for all signature checks. The kid
returned here is the only kid this substrate signs with; receipts whose
signatures[].kid does not match (and which lack any other resolvable kid)
MUST be rejected.
1.3 GET /attestation/v1/log
{
"leaf_count": 5,
"root": "16d63c30139e288d67c3aa08b36acdb9b41185c239adfa0b270453f8837d5e1e"
}
The current root advances monotonically as receipts are appended. Verifiers SHOULD pin a fetched root for the duration of a single verification so that intervening appends don't shift the ground truth mid-flight.
1.4 GET /attestation/v1/receipt/{id}
Returns the signed canonical receipt plus the log mapping recorded at the time of inclusion.
{
"receipt": { /* full v1 signed receipt object */ },
"leaf_index": 0,
"leaf_count_at_inclusion": 1,
"root_at_inclusion": "<hex>",
"log_leaf_count_now": 5,
"log_root_now": "<hex>"
}
leaf_count_at_inclusion and root_at_inclusion are the values the
substrate observed immediately after appending this receipt. They are
informational; for verification, callers should rely on the proof returned
by /proof/{id} and the current /log.
If the receipt is unknown: HTTP 404 with {"error":"receipt not found"}.
1.5 GET /attestation/v1/proof/{id}
Returns an inclusion proof in the format defined by
/merkle/log/v1 §6, augmented with the root the
proof is against:
{
"leaf_index": 0,
"leaf_count": 5,
"siblings": ["<hex>", ...],
"bag_peaks_left": ["<hex>", ...],
"bag_peaks_right": ["<hex>", ...],
"root": "<hex>",
"id": "<hex>"
}
The root field is the current log root at the time the proof was generated.
Verifiers MUST cross-check this matches /attestation/v1/log (within their
pinning window).
1.6 POST /attestation/v1/append
Body: a v1 receipt object (without id or signatures — those are
populated by the substrate).
The substrate:
- Strips any caller-supplied
idandsignatures. - Computes the receipt's canonical id.
- Signs with the substrate's private key.
- Canonicalizes the SIGNED receipt and appends those bytes as the next MMR leaf.
- Persists: receipt blob (by id), log snapshot, id↔leaf-index index.
- Responds:
{
"id": "<receipt id, hex>",
"leaf_index": 0,
"leaf_count": 1,
"root": "<hex>",
"canonical_size_bytes": 1141
}
Errors:
400— body is not parseable JSON or fails canonicalization.409— receipt with this id is already in the log (deduplication).500— persistence failure.
Note on access control for v1: this endpoint is unauthenticated in the
reference implementation. In production, gate POST /append to authorized
substrate-internal callers (mTLS or capability-token auth — the substrate
should only sign receipts it itself produced).
2. Verification flow (normative)
A correct verifier performs these steps in order:
GET /attestation/v1/trust-root→ cache{issuer_did, kid, public_key}.GET /attestation/v1/receipt/{id}→ getreceipt.- Recompute the receipt id from
BLAKE3(canonical(receipt minus {id, signatures})). It MUST matchreceipt.idAND the{id}requested. - Verify signatures. Build the signing payload:
"deos-receipt-v1:" || canonical(receipt minus {signatures}). Verify each signature inreceipt.signatureswhosekidis recognized. At least one must verify and its kid must belong toissuer_did. GET /attestation/v1/proof/{id}→ get{leaf_index, leaf_count, siblings, bag_peaks_left, bag_peaks_right, root}.GET /attestation/v1/log→ cross-check that the log's currentrootandleaf_countmatch what the proof was issued against.- Verify the inclusion proof per
/merkle/log/v1§6.2. The leaf data iscanonical(full signed receipt)— the log binds the signed instance, not the unsigned body. - (Future, M5) If
receipt.anchorsis non-empty, verify at least one anchor proof and treat the anchor's timestamp as the receipt's time-of-existence floor.
If any step fails, the receipt is rejected with a specific error code:
| Code | Meaning |
|---|---|
INVALID:id |
Recomputed id does not match claim or request |
INVALID:issuer |
receipt.issuer differs from trust-root issuer DID |
INVALID:alg |
Unrecognized signature algorithm |
INVALID:sig |
Signature does not verify, no signature by issuer kid, or sig encoding broken |
INVALID:proof |
Inclusion proof does not yield the published root |
INVALID:root |
Proof's stated root disagrees with /log |
INVALID:signatures |
Signatures array missing or empty |
The reference verifier implements this flow and is intentionally compiled with no shared crate with the receipt-format reference impl or the attestation server. Spec-only interoperability is the explicit goal.
3. Trust assumptions (v1)
What v1 trusts:
- The substrate's private key is uncompromised. (M7 introduces revocation; M5 introduces external time anchors that bound the damage of post-hoc key compromise.)
- The verifier's TLS connection to the API is authenticated. In the reference impl this is plain HTTP; production deployments MUST use HTTPS with a known TLS roots set.
- BLAKE3-256 and Ed25519 are unbroken.
What v1 does not trust:
- The substrate is honest about which receipts it has issued — verifiers do not assume "if it's in the log it's legitimate"; they require a signature from the substrate's published kid.
- The substrate maintains availability — receipt + proof can be cached by verifiers and replayed offline as long as they hold the trust-root public key.
4. Tampering taxonomy (caught by the verifier)
| Tamper | Detected at step |
|---|---|
| Mutate any byte of the receipt body | (3) id mismatch, then (4) signature would also fail |
| Strip a signature | (4) issuer signature missing |
| Substitute a forged signature | (4) signature does not verify |
| Send a different receipt under the requested id | (3) recomputed id ≠ requested id |
| Tamper with the inclusion proof | (7) proof does not yield root |
Forge a fake root in /log |
(4) the root only matters for inclusion verification — the receipt is still independently signed; a fake root makes proofs fail (7), it doesn't admit a forged receipt |
| Append a fake receipt to the log without a substrate signature | (4) verifier rejects: no valid issuer signature |
5. Operational notes
- Persistence layout used by the reference server:
log.bin MMR snapshot (atomic .tmp + rename) index.json id -> {leaf_index, leaf_count_at_inclusion, root_at_inclusion} receipts/<id>.json key.json substrate signing key (Ed25519, NEVER commit) - Production substrates MUST issue the signing key through an HSM or controlled key-ceremony process, never auto-generate.
6. Open questions deferred
- Concurrency. A single-mutex append is fine for a reference impl; production needs append serialization with read concurrency and batched fsync.
- TLS termination. Production deployments MUST terminate TLS upstream.
- Auth on
/append. Open in v1 for demo; production gates by mTLS or capability token. - Pagination on
/log. No list-all-receipts route — discovery is by id from receipts the holder already has. - WebSocket / SSE for live root advances. v1.1.
- CORS. Browser SDKs will need it; not in v1.
- Streaming proof verification. Consistency-proof range verification for very large logs. v1.1.
7. Maintainer
DEOS Computing — design questions, conformance reports, and proposed additions to github.com/DEOS-Computing.
License: CC BY 4.0 (text), Apache-2.0 (reference code).