# The Heartwood Seal Protocol

**Version 1.4 — 2026-09-07** *(1.1 added the `countersign` entry kind, §5.4. 1.2 added optional embedded documents to the `seal` kind, §5.3. 1.3 adds the owner-signed `share-census` governance kind: `{ kind, counts, totalShares, authorKeyId, signature }`, an auditable snapshot of anonymous share tallies — counts mapped by content id, never identities. It verifies under the owner-signed governance rule in §7. Verifiers written against earlier versions must add the corresponding rules. 1.4 adds bounded key delegation, §5.5: the `key-delegation` kind — `{ kind, sessionKeyId, sessionPublicKeyHex, expiresAt, maxSeals, label, authorKeyId, signature }`, signed by an active granted master key, where `sessionKeyId` MUST equal the first 16 hex characters of sha256 over `sessionPublicKeyHex`, `expiresAt` MUST be after the block's own timestamp, `maxSeals` is a positive integer or `null`, and the session key id MUST NOT collide with any granted key — and the `delegation-revoke` kind — `{ kind, targetSessionKeyId, note, authorKeyId, signature }`, valid only when signed by the delegating master or an owner-role key. A `seal` entry whose `authorKeyId` is not a granted key is valid if and only if a prior `key-delegation` names it, unrevoked at that point, with the block timestamp strictly before `expiresAt`, the count of prior session seals under that delegation strictly below `maxSeals` when non-null, the delegating master active, and the entry signature verifying against `sessionPublicKeyHex`. Session keys may sign nothing but `seal` entries.)*

An open protocol for public statements that are provably authored, provably unedited, and permanently citable, with a named party accountable for every word.

**License:** This specification is published under the [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/) license (CC BY 4.0). Anyone may implement it, in any software, for any purpose, without permission or fee. The publisher holds no patents over anything described here and commits to never asserting any intellectual-property claim against an independent implementation. The reference verifier (`verify-seal.js`) and the reference sealing client (`seal-document.js`) are MIT licensed so they can be copied into any codebase.

**Status:** Version 1.0 describes the protocol as implemented by the Heartwood Library reference implementation. The chain block format is versioned by `formatVersion` (currently `1`) and proof bundles by `proofVersion` (currently `1`); incompatible changes will increment these.

---

## 1. Design goals

1. **Append-only.** Nothing recorded can be edited or deleted, by anyone, including the operator. Corrections are new records that supersede, never replace.
2. **Accountable.** After ownership is established, every record is signed by a key granted on the chain itself, and the grant history is part of the same permanent record.
3. **Independently verifiable.** Any third party can verify any record with no trust in the serving operator, using only published algorithms and the record itself.
4. **Minimal.** The whole protocol rests on two primitives: SHA-256 and Ed25519. A conforming verifier is a few hundred lines with no dependencies.

## 2. Terminology

- **Chain** — an ordered list of blocks, starting with a genesis block.
- **Block** — one sealed unit: metadata plus one entry (the genesis block's entry is `null`).
- **Entry** — the payload of a block; its `kind` field selects its schema (§5).
- **Seal / sealing** — appending an entry as a new block. A sealed thing's public identifier is its block hash.
- **Steward** — the process holding a signing key that appends blocks.
- **Owner** — a key granted with the `owner` role; owners govern grants, taxonomy, and moderation.
- **Open era** — blocks appended before the first key grant; they are unsigned and remain valid forever.

## 3. Cryptographic recipes

All hashing is **SHA-256**, rendered as 64 lowercase hex characters. All text is hashed as its UTF-8 bytes.

All signatures are **Ed25519**. Public keys travel as the hex encoding of the SPKI DER form (`publicKeyHex`). Signatures travel as base64. The signed message is always a UTF-8 canonical-JSON string (§3.1).

- **Key id** — the first 16 hex characters of `sha256(publicKeyHex)`, where `publicKeyHex` is hashed as a UTF-8 *string*, not as decoded bytes.
- **Content hash** (`contentHash`, articles and comments) — `sha256(content)`.
- **Statement hash** (`statementHash`, document seals) — `sha256(statement)`.
- **Document hash** (`documentHash`, document seals) — `sha256` of the raw document bytes. The document itself never enters the chain.

### 3.1 Canonical JSON

The canonical JSON of a value is JSON with **object keys sorted lexicographically at every depth** and **no whitespace**. Arrays keep their order. Strings, numbers, booleans, and `null` are encoded exactly as `JSON.stringify` encodes them. Two structurally equal objects always produce identical bytes; every hash and signature below is computed over canonical JSON.

### 3.2 Block hash

```
blockHash = sha256(canonicalJson({
  entry:         <the entry, exactly as stored, signature included>,
  formatVersion: <number>,
  index:         <number>,
  previousHash:  <hex string>,
  timestamp:     <ISO 8601 string>
}))
```

(Key order above is alphabetical because canonical JSON sorts keys; listing them in any order yields the same bytes.) The block's public id **is** its hash: an identifier that cannot be remapped to different content.

### 3.3 Signature payload

A signature never covers the entry alone; it covers the entry **bound to its exact chain position**:

```
payload = canonicalJson({
  entry:        <the entry with its `signature` field removed>,
  index:        <the block index being signed for>,
  previousHash: <the hash of the block immediately before it>
})
signature = base64(ed25519_sign(payload))
```

This binding means a signature cannot be replayed at another position or onto another chain.

## 4. Blocks and the chain

A block is a JSON object: `{ formatVersion, index, timestamp, previousHash, entry, hash }`. The reference implementation stores one block per line (JSONL). The genesis block has `index 0`, `previousHash` of 64 zeros, and `entry: null`.

**Chain validity** (§7) requires every block's `index` to equal its position, every `previousHash` to equal the prior block's `hash`, and every `hash` to recompute correctly. Appending is the only write operation a conforming implementation may have.

## 5. Entry kinds

Every entry carries `kind` (absent means `article`, for open-era compatibility) and, once ownership is established, `authorKeyId` and `signature`. Nullable fields (`supersedes`, `parentCommentId`, `documentName`, `documentData`, `documentType`, `categories`, `note`) are stored as explicit `null` when absent. Three fields are **omitted entirely** when absent, never stored as `null`: `gate`, `authorKeyId`, and `signature`. This distinction is normative — canonical JSON of `{"gate":null}` and of an object without `gate` differ, and hashes with them.

### 5.1 `article`

`{ kind, topicPath, title, author, content, contentHash, supersedes, commentsOpen, gate?, authorKeyId?, signature? }`

`topicPath` is an array of 1–6 segments (≤60 chars each). `title` ≤200 chars; `author` ≤120; `content` ≤400,000. `supersedes` is a prior article's block hash or `null`. `commentsOpen` is a boolean fixed at publication. `gate`, when present, is `{ gated: true, categories: [...], source }` — a transparent labeling marker sealed at publication.

### 5.2 `comment`

`{ kind, targetEntryId, parentCommentId, content, contentHash, gate?, authorKeyId, signature }`

`targetEntryId` must be an earlier article block with `commentsOpen: true`; `parentCommentId`, when not `null`, must be an earlier comment on the same target. `content` ≤5,000 chars. Comments carry no display-name field by design: the name is always the on-chain grant name of the signing key.

### 5.3 `seal` (document seal)

`{ kind, title, author, statement, statementHash, documentHash, documentName, documentData, documentType, gate?, authorKeyId, signature }`

Notarization, with or without publication, the sealer's choice. `documentHash` fingerprints the document; `statement` (≤2,000 chars) is the public description; `documentName` is an optional label (≤160). By default the document itself never enters the chain (`documentData` and `documentType` are `null`). The sealer MAY include the document: `documentData` is the standard base64 of the raw bytes (≤262,144 bytes decoded) and `documentType` its media type. **The embedded bytes MUST hash to `documentHash`** — an included copy is self-proving, and a verifier MUST reject a seal whose embedded bytes do not match their fingerprint. Seals require established ownership; there are no open-era seals, because accountability is the product.

### 5.4 `countersign` (agreements)

`{ kind, targetSealId, documentHash, statement, statementHash, gate?, authorKeyId, signature }`

A co-signer's seal on an existing document seal — how a seal becomes a multi-party agreement. `targetSealId` is an earlier `seal` block's hash. `documentHash` **must equal the target seal's** `documentHash` and is carried in the countersign itself, so each party's signature provably covers the document's bytes, not merely a pointer. `statement` (≤500 chars, e.g. "Agreed and accepted for Acme Corp") is optional; when `null`, `statementHash` is `null`, otherwise `statementHash = sha256(statement)`. One agreement, one set of bytes, many accountable names — each countersign is its own block with its own proof bundle. Invitation workflows, and who pays for a countersign, are operator policy outside this protocol; the chain records only the completed signatures.

### 5.5 Governance kinds

- **`key-grant`** — `{ kind, keyId, publicKeyHex, name, role, authorKeyId, signature }`. `role` is `owner` or `contributor`. The **bootstrap grant** is the first grant on a chain: the key grants itself with the `owner` role and its own signature establishes ownership. Every later grant must be signed by an active owner key. A grant may not target a currently active key.
- **`key-revoke`** — `{ kind, targetKeyId, authorKeyId, signature }`. Ends the target key's writing from this block forward; everything it sealed before remains valid and attributed. The last active owner key can never be revoked.
- **`taxonomy`** — `{ kind, operation, ..., authorKeyId, signature }` with `operation` one of `move` (`fromPath`, `toPath`), `order` (`parentPath`, `childOrder`), `describe` (`topicPath`, `description`). Taxonomy is a display overlay; recorded entries are never touched.
- **`moderation`** — `{ kind, action, targetEntryId, categories, note, authorKeyId, signature }` with `action` one of `gate` (label behind a notice), `withhold` (stop serving the text; the sealed bytes remain on the chain as proof it existed), `ungate`. Owner-signed only. Moderation history is itself public and permanent.
- **`censorship-request`** — an owner-sealed public record of a demand received (`authority`, `jurisdiction`, `demandType`, `summary`, `platformResponse`, `reference`, …): the transparency log.

## 6. Sealing protocols

### 6.1 Steward sealing

A process holding a granted active key builds the entry, signs the payload (§3.3) for the current tip position, and appends the block.

### 6.2 Remote sealing (sign-where-the-key-lives)

The author's private key never travels. Two steps:

1. **Prepare.** The author sends the raw fields plus `authorKeyId`. The server validates, screens, and returns `{ entry, payload, expectedIndex, expectedPreviousHash }` — the exact entry it will accept and the exact payload to sign, bound to the current tip.
2. **Submit.** The author signs `payload` locally and returns `{ entry, signature, expectedPreviousHash }`. The server **rebuilds the entry from its raw fields and requires the canonical JSON of the rebuild to match the submission byte for byte** (nothing can be smuggled), verifies the signature against the on-chain grant over the tip position, and seals. If the tip moved between the steps, the submission is refused as retryable (HTTP 409 in the reference implementation): prepare and sign again.

## 7. Verification

A conforming verifier replays the chain from genesis and rejects it on the first failure:

1. Indexes are sequential from 0; each `previousHash` links; each `hash` recomputes (§3.2).
2. Key state is replayed from grants and revokes, enforcing the bootstrap rule, owner-signing of later grants and of revokes, the no-regrant-of-active-keys rule, and the last-owner protection.
3. After ownership is established, **every** entry must verify (§3.3) against a key that was granted and active *at that block*.
4. Per kind: articles and comments must have `contentHash = sha256(content)`; comments must target an earlier open article (and parent on the same target); seals must have `statementHash = sha256(statement)`, a well-formed `documentHash`, and, when `documentData` is present, embedded bytes that hash to `documentHash`; countersigns must target an earlier `seal` block, carry a `documentHash` equal to the target's, and keep statement/statementHash consistent (§5.4); governance kinds must be owner-signed; moderation actions, taxonomy operations, and demand types must be from their closed sets.
5. Revocation is forward-only: entries sealed while a key was active remain valid after its revocation.

## 8. Proof bundles

`proofVersion 1`. A self-contained proof of one sealed block:

```
{ proofVersion, generatedAt, block, author: { keyId, name, publicKeyHex, activeNow } | null,
  signed, tip: { index, hash }, recipes: { ... } }
```

`block` is the exact stored block. Independent verification of a proof checks: the block hash recomputes; the internal text hashes match; the signature verifies over the §3.3 payload with `author.publicKeyHex`; the key id derives honestly from the public key; and, optionally, that the holder's own copy of the content or document hashes to the sealed fingerprint.

**What a proof cannot show alone:** that the block is part of the chain the rest of the world sees. That assurance comes from tip witnessing (§9) and grows with every independent observer.

## 9. Tip attestation

`{ index, hash, blockCount, attestedAt, attestation: { keyId, payload, signature } | null }`, where `payload = canonicalJson({ attestedAt, hash, index })` signed by the steward. Anyone recording attested tips over time can prove a rewrite: an old signed tip absent from the current chain is evidence of tampering. Independent tip witnesses are the decentralization path in miniature.

## 9.1 Public anchoring (informative)

Outside consensus, the operator MAY stamp each block's coordinates into
Bitcoin through OpenTimestamps. The stamped bytes are the canonical JSON of
`{what:"heartwood-block-anchor", v:1, index, hash, sealedAt, url}` — the
`url` is the cross-reference back to the block's page on the library. Only
the sha256 of that statement reaches the calendars, and Bitcoin commits to
an aggregated merkle root: no content, no author names, no payment, no
token custody. Statements and detached `.ots` proofs are served under
`/api/anchors`; any copy of the open `ots` client verifies them against
Bitcoin with no trust in the library's server. Verifiers MUST NOT treat the
absence of an anchor as invalidity (anchoring is best-effort and
asynchronous), but a confirmed anchor gives every block beneath it an
independent latest-possible-time bound that even the operator cannot move.

## 10. Reverse lookup

Verification as a service: given a sha256 fingerprint, return every sealed thing matching it — document seals and countersigns by `documentHash`, articles and comments by `contentHash`. For an agreement, a single lookup therefore returns the original seal and every party's countersign. A fingerprint miss means only that these exact bytes were never sealed on this chain; it is not evidence of inauthenticity.

## 11. Security considerations

- A single-operator chain makes tampering *detectable*, not impossible: an operator with file access could rebuild history from some point. Detection rests on §9 witnesses and on independently held copies of the chain. Implementations should say this plainly.
- The screening step in §6.2 runs before sealing because sealing is irreversible; personal-data refusal at the door is how an undeletable record coexists with privacy law.
- `withhold` governs display surfaces only. The raw chain remains the audit surface; the sealed record of a withheld entry (hashes, block, signature) stays public so the act of withholding cannot itself be hidden.
- Commercial state (payment, credit balances) MUST stay off the chain: a balance is mutable; speech is not.

## 12. Conformance

A **conforming chain implementation** appends per §4–§6, verifies per §7 at load, and refuses to operate on an invalid chain. A **conforming verifier** implements §8's checks with no reliance on the serving operator. A **conforming public face** serves reads and the remote-sealing endpoints without holding any signing key.

---

*Reference implementation: the Heartwood Library (`src/chain.js`, `public/verify-seal.js`, `scripts/seal-document.js`). This document is the protocol; where prose and reference implementation disagree, the disagreement is a bug to be fixed openly.*
