When you read a Hypermedia document, you read bytes that came from somewhere: a site, a peer, a cache. Integrity covers what you can check about those bytes yourself, without trusting whoever handed them to you. You can verify some things from the content alone. Other things are trusted, and you need to know which.
This page lists both, as the daemon implements them today, and then the mitigations for the trusted parts.
What is verified end to end
Content addressing. Every blob is named by the hash of its bytes, a CID. When a blob arrives with a CID attached, whether over the daemon's store RPC, over POST /ipfs/<cid>, or through Bitswap, the daemon recomputes the hash and refuses a mismatch. So a link to a CID is a link to exactly one sequence of bytes, forever, on any node. This makes the end of broken links possible.
Authorship. Every structural blob (Change, Ref, Comment, Capability, Contact, Profile) carries the signer's public key and a signature, Ed25519 or ECDSA P-256 depending on the key, over its canonical DAG-CBOR encoding with the signature field zeroed. The daemon verifies the signature before interpreting the blob. A blob with a bad signature is at most stored as opaque bytes, and is never indexed. So you can re-check "signed by z6Mk…" anywhere, without asking a server. See Blobs for the exact signing rule.
History. A document version is a set of Change CIDs. Each Change links its dependencies and its genesis by CID, so a version pins its entire history as a Merkle DAG. Changing anything in the past changes every CID after it. Replay rejects changes whose declared depth or time runs backwards relative to their dependencies. It also rejects a Ref whose Changes do not share a genesis. See Documents.
Write authority. The daemon indexes only Refs signed by the space owner or by a key holding a matching owner-signed capability. Refs and capabilities are both signed and content-addressed. So a third party can re-verify the whole chain offline: this Ref, this capability, this owner. The rule is in Permissions.
Together, these let a reader who holds a version's CIDs verify three things with no network and no trusted party: who wrote each change, in what order, and that the person who published this version was allowed to.
What is trusted
Timestamps are self-declared. Nothing checks a blob's ts against real time when it is ingested. Ordering inside a document is causal (dependencies). But last-writer-wins resolution of metadata, comment ordering, and generation numbers all read declared times, so a legitimate signer can back-date or forward-date. A capability issued after a Ref authorizes it retroactively. If you need a trustworthy time, anchor the CID externally. The team has discussed OpenTimestamps, but nothing is built.
Nothing checks authorization on Changes and comments. The daemon stores any key's Change. The Change becomes part of a document only through an authorized Ref. Any key can attach a public comment to any document. No layer checks it: not the index, not the API. Moderation is left to clients and sites.
Capabilities never expire and cannot be revoked. A grant is valid on every node that has it, forever. See Permissions for the mitigation: grant narrowly, to keys you control.
Peer identity is separate from account identity. A peer proves it holds an account only when it wants private data, with a signed ephemeral capability fresh within one minute. Public data needs no peer-level trust, because public blobs verify themselves.
Site trust is DNS and HTTPS trust. The peer that https://<siteUrl>/hm/api/config reports is granted the space's private blobs, because the home document says that host is the site. Whoever controls the domain and its certificate controls that trust. A space owner who points siteUrl at a hostile host hands that host their private content.
The local daemon API has no authentication. The gRPC port and the HTTP port accept any caller who can reach them, with CORS open to every origin. Any such caller can list keys, sign with any stored key, export keys, store blobs, create Refs and capabilities with any registered key, or force a reindex. Write RPCs check whether the key may write. They never check the caller. The daemon is designed to be reached only from localhost or behind a firewall. Hosted sites run with -public-only, which hides private content from unauthenticated HTTP requests but does not disable writes.
Bearer tokens are symmetric and daemon-local. A token is sealed with a random per-daemon secret and lasts thirty days. The only way to invalidate it is to rotate that secret. Its only tie to state is that the daemon must still know the principal. It widens reads on public-only nodes and never authorizes a write.
Two CIDs for the same bytes. The daemon hashes with BLAKE2b by default and the SDK hashes with SHA-256. Both are valid CIDs of the same content. The daemon deduplicates by multihash, so a Ref naming the SHA-256 head and another naming the BLAKE2b head of identical bytes are two different documents. Always pass explicit CIDs for blobs that other blobs reference. See Blobs.
Mitigations
Trusted part | What to do about it |
|---|---|
open local API | bind to localhost, firewall the ports, run hosted nodes with |
unauthenticated comments | filter by author or by contacts in the client; site-level comment gating is planned |
eternal capabilities | grant WRITER on a path to a key you control; give bots and servers their own keys; treat a lost delegate key as permanent |
self-declared time | treat |
site trust | keep control of the domain and certificate; the site's account and signer are visible in |
dual CIDs | always pass a |
Compared with ActivityPub
Federated social protocols such as ActivityPub verify a message at the transport. The receiving server checks an HTTP signature from the sending server, then stores the content as its own copy under its own authority. If that server goes away or changes its mind, the content and its provenance go with it. Hypermedia puts the signature on the content. Every blob carries its author's signature and its own hash. Any copy, on any node, years later, still proves who wrote it and that nothing changed. Servers are replicas with no authority over the content. The longer argument, and the trade-offs, are in Why signed content.
Working with integrity
In the Seed app
The Seed app verifies signatures and CIDs on everything it indexes through the local daemon. It shows the author of each change in a document's history, and stashes unauthorized Refs. Verification is always on, and there is no switch for it.
CLI
seed-cli blob get <cid> # fetch the raw signed blob
seed-cli blob verify <cid> # check the signature of a stored blob
seed-cli document changes hm://… # the change DAG with signers and times
seed-cli document cid hm://… # the CIDs behind a versionSee the CLI guide.
SDK
blobs.verify checks a signature against the embedded signer. signed-blob handles the canonical encoding and the zeroed-signature rule so your own blob types verify the same way. Compute CIDs with multiformats and pass them explicitly on publish. See SDK.
Web API
On the Seed API, GET /api/GetCID?cid=… returns a blob as DAG-JSON for inspection. GET /api/ListChanges?targetId=… walks a document's history with signers. /ipfs/<cid> on a site serves raw bytes whose hash you can recompute. On the read side you trust the site only to be available.
Agents
Seed Agents read through the same site APIs and can inspect a blob with read on an ipfs://<cid> address. When an agent publishes, the agent server signs with the agent's own key. For delegated writes it attaches the capability CID so a receiving node can re-verify authority. The runtime's own signed envelope uses the same blob signing scheme with a time window; see Signed API.
Do you like what you are reading? Subscribe to receive updates.
Unsubscribe anytime