# Post Signing

Every post published under `/writing/` on chrischerry.me is signed. Readers can independently verify that a post was authored by Chris Cherry and has not been modified since publication.

## TL;DR — verify a post in three commands

```
SLUG=2026-02-27-preemption-without-doctrine
curl -sS -O https://chrischerry.me/writing/$SLUG.json
curl -sS -O https://chrischerry.me/writing/$SLUG.json.sig
curl -sS -O https://chrischerry.me/verify/allowed_signers

ssh-keygen -Y verify \
  -f allowed_signers \
  -I chris@chrischerry.me \
  -n chrischerry.me-post \
  -s $SLUG.json.sig < $SLUG.json
```

A valid signature prints:

```
Good "chrischerry.me-post" signature for chris@chrischerry.me with ED25519 key SHA256:uKR8I5F+oF1Wqm8uvuRqcHbMhLXrt4vAQCPFlvjnpNE
```

Any other output — no output, `bad signature`, key mismatch — means the post has been modified or is not authentic.

## Canonical JSON

The signed artifact for each post is a canonical JSON document served at `/writing/<slug>.json`. Its bytes are the exact input to the signature, so verifiers must not re-serialize before verifying.

Fields, in the order they appear after RFC 8785 canonicalization (which sorts object keys lexicographically):

| Field            | Type         | Description                                                              |
| ---------------- | ------------ | ------------------------------------------------------------------------ |
| `aiAssisted`     | boolean      | True if AI tools contributed to research or drafting.                    |
| `author`         | string       | Author name: `"Chris Cherry"`.                                           |
| `body`           | string       | Full post body, verbatim.                                                |
| `canonical_url`  | string       | The published URL of the post.                                           |
| `content_version`| integer      | Increments on every re-sign where content changed (starts at 1).         |
| `post_id`        | string       | Immutable slug matching the post URL.                                    |
| `published_at`   | string       | Original publication date as ISO 8601 UTC.                               |
| `revised_at`     | string       | Last-revision date as ISO 8601 UTC (equals `published_at` if unrevised). |
| `tags`           | string array | Topic tags, order preserved.                                             |
| `thesis`         | string       | One-paragraph thesis of the piece.                                       |
| `title`          | string       | Title.                                                                   |
| `wordCount`      | integer      | Word count of `body` (whitespace-split).                                 |

### Canonicalization

JSON is serialized per [RFC 8785 (JCS)](https://datatracker.ietf.org/doc/html/rfc8785). A reference implementation is published at [`/verify/canonicalize.mjs`](https://chrischerry.me/verify/canonicalize.mjs) so verifiers can independently reproduce the byte sequence:

```js
import canonicalize from 'https://chrischerry.me/verify/canonicalize.mjs';
const post = await (await fetch('/writing/<slug>.json')).json();
canonicalize(post) === <original response body>;   // true
```

## Signing Key

Dedicated Ed25519 key used only for signing post content:

```
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGhSrmkNVrOA2Cfknnk0giYUgEsPiyjm/zoGarMJUt7b
SHA256:uKR8I5F+oF1Wqm8uvuRqcHbMhLXrt4vAQCPFlvjnpNE
```

- Public key: [`/verify/signing-key.pub`](https://chrischerry.me/verify/signing-key.pub)
- `allowed_signers` file: [`/verify/allowed_signers`](https://chrischerry.me/verify/allowed_signers)
- Signing namespace: `chrischerry.me-post`
- Signing identity: `chris@chrischerry.me`

### Binding to PGP identity

The Ed25519 key is bound to my PGP identity (`0443E27B9D24ECC7DFFED2EB18355095981ECD6C`) via a GPG-signed attestation: [`/verify/signing-key-attestation.txt.asc`](https://chrischerry.me/verify/signing-key-attestation.txt.asc). Readers who already trust the PGP key can verify the attestation and trust the Ed25519 key transitively.

```
curl -sS https://chrischerry.me/pubkey.asc | gpg --import
curl -sS https://chrischerry.me/verify/signing-key-attestation.txt.asc | gpg --verify
```

## Versioning

`content_version` starts at 1 for a new post. It increments only when the canonical content (every field except `content_version`) actually changes — re-signing an unchanged post produces byte-identical JSON and signature. Typo fixes and substantive updates both bump the version.
