# Chain-head signing & anchoring

The hash chain proves a tenant's books are *internally consistent* — no entry was
altered without the head changing. Signing + anchoring add two things on top:

1. **Signing (Ed25519)** — Veritally signs the chain head with its private key.
   Anyone with the public key can prove *this head was attested by Veritally* — a
   third party can't forge a "Veritally-signed" head. This is what gives the
   "Verified by Veritally" badge cryptographic weight instead of just being our word.
2. **Anchoring** — each signed head is appended to a per-tenant, **append-only,
   hash-chained anchor log**. Publish the latest anchor hash to a witness you don't
   control and, from that point on, the history-before-then is frozen.

## What each layer does — and does NOT — guarantee

Being precise here matters (this is an EA-credibility product; no security theater):

| Threat | Hash chain | + Signing | + Anchoring **and external publication** |
|---|---|---|---|
| Someone edits a stored entry | ✅ detected (head changes) | ✅ | ✅ |
| Someone forges a "Veritally-verified" head | ❌ | ✅ signature won't verify | ✅ |
| **The operator (us) silently rewrites history + re-signs** | ❌ | ❌ (we hold the key) | ✅ the published anchor contradicts the rewrite |

**Signing alone does not stop the key holder from re-signing a rewritten head** —
we hold the private key, so on its own it's non-repudiation *to third parties*, not
immutability *against us*. The honest "we can't silently rewrite your books either"
guarantee comes from **anchoring + publishing the anchor head to an external witness**
(a public post, a git commit, an OpenTimestamps proof, a customer-held copy). Once
anchor hash *H* is public at time *T*, we cannot present a different history-before-*T*
without contradicting *H*.

## Key custody

- **Private key** (`SIGNING_KEY`, Ed25519 PKCS8 base64) is a **Worker secret** — never
  in the repo, never in D1. Mint it with `node tools/gen-signing-key.mjs`.
- **Public key** (`SIGNING_PUBKEY`, SPKI base64) is public — set it as a Worker var and
  it's served at `/.well-known/veritally-signing-key.json`.
- This deployment uses **one service key** for all tenants: it proves *Veritally*
  attested a head. It deliberately does **not** claim each customer holds a key we
  can't forge — that would require customer-held keys, a bigger change. The immutability
  guarantee is delivered by anchoring + publication, and that's what we claim.
- Everything is **gated**: with no `SIGNING_KEY` set, `/v1/anchor` returns 501 and the
  cron anchor step is a no-op — the product runs exactly as before.

## Surfaces

| Route | Purpose | Auth |
|---|---|---|
| `POST /v1/anchor` | sign the current head + append an anchor | admin key |
| `GET /public/anchors/:slug` | the signed, hash-chained anchor log + public key | none (opt-in) |
| `GET /.well-known/veritally-signing-key.json` | the service public key | none |
| `GET /public/proof/:slug` | now includes `signed` (latest anchored head) | none (opt-in) |
| `GET /proof/:slug` | the page **verifies the Ed25519 signature in-browser** | none (opt-in) |

The daily cron (`0 6 * * *`) anchors every proof-enabled tenant automatically.

## Verify a signed head yourself

The signed payload is a fixed canonical string:

```
veritally-head-v1|<tenant>|<ledger_seq>|<head>|<ts>
```

Fetch the public key from `/.well-known/veritally-signing-key.json` and the latest
`signed` block from `/public/proof/:slug` (or an entry from `/public/anchors/:slug`),
rebuild that string, and verify the base64 `sig` with any Ed25519 tool. The proof
page does exactly this with the browser's native `crypto.subtle` — so "valid" is your
computer's verdict, not ours.

## Operating it

1. `node tools/gen-signing-key.mjs` → `wrangler secret put SIGNING_KEY`; put
   `SIGNING_PUBKEY` in `wrangler.toml [vars]`.
2. Apply `migrations/0004_anchors.sql` (additive, safe to re-run).
3. Redeploy. Anchoring begins on the next daily cron, or `POST /v1/anchor` now.
4. **To earn the immutability claim:** publish the daily anchor head somewhere you
   don't control (a scheduled public post, a git repo, OpenTimestamps). Until you
   publish externally, anchoring is tamper-*evident* but not operator-*proof*.
