# Signing

Edition 2026-08-14.

Ed25519 over the canonical pre-image. One algorithm, no negotiation: an
algorithm field a verifier reads from the thing being verified is a downgrade
surface, so `algorithm` is present for forward compatibility and any value other
than `ed25519` is refused rather than dispatched on.

## The pre-image

```
GAVR-SIG/v1/<context>\n  ++  canonical JSON of { "meta": <meta>, "payload": <payload> }
```

The prefix is UTF-8, ends in a single newline, and is followed immediately by
the canonical bytes. `<context>` is the signing context, from a closed set.

`meta` is the signature's own metadata minus its `value`:

| Field       | Meaning                                                   |
| ----------- | --------------------------------------------------------- |
| `algorithm` | `ed25519`                                                 |
| `context`   | Domain separation, from the closed set                    |
| `signer`    | `{ kind, id }` — who this signature claims to be          |
| `custody`   | `SELF` or `HOSTED`                                        |
| `created`   | RFC 3339                                                  |
| `publicKey` | base64url of the raw 32-byte Ed25519 public key           |
| `keyId`     | `ed25519:` + base64url of SHA-256 over the raw public key |

`value` is base64url of the raw 64-byte signature. base64url throughout is
unpadded.

## The metadata is inside the pre-image, and that is load-bearing

If only the payload were signed, an attacker could take a valid signature and
rewrite `custody` from `HOSTED` to `SELF`, silently upgrading _"the hosting
instance signed this as the authority"_ into _"the authority signed this"_ on a
signature that still verifies. The same argument covers `signer`, which decides
whose record this is, and `context`, which is the whole of the domain
separation.

**Anything a relying party reads off a signature has to be covered by it.**

Corpus case: `covers its own metadata, so custody cannot be rewritten`.

## Domain separation

The context is in the **prefix**, not merely inside the JSON. A signature made
for one context must not verify in another, or a record signature becomes a
secretariat action signature. Prefixing is what makes that structural rather
than a check somebody can forget.

Contexts are a closed set, one per kind of thing that gets signed: the record
document, a registry acceptance, a secretariat action, a participant action.

## Custody: what each mode actually proves

| Mode     | Claim                                                      |
| -------- | ---------------------------------------------------------- |
| `SELF`   | The authority holds the key and signed this.               |
| `HOSTED` | A node bound to the authority signed **as** the authority. |

These are not equivalent and a surface presenting them as equivalent is
misleading its reader. Self-custody is the stronger claim. Hosted custody means
the private key is on somebody else's machine, which is why the transport floor
differs: **hosted custody requires mutual TLS and self-custody does not** — under
hosting, the client certificate is the only thing tying a request to the node
the authority actually authorised.

The record states which mode produced each signature, so a consumer can weigh
it. A declared custody that disagrees with the one the registry resolves from
the presented credentials is refused, because a signature claiming `SELF` on a
hosting node's key is a stronger claim than the facts support.

## Verification is not authorization

`verify` answers exactly one question: **are these bytes a valid signature by the
key this signature names?**

It does not answer whether that key belongs to a registered, unrevoked
authority. That is a lookup against the registry's key directory. An
implementation treating a successful verification as authorization accepts a key
an attacker generated thirty seconds ago, which is a complete bypass.

Two further requirements:

- **`keyId` must match `publicKey`.** Refuse the signature otherwise. A
  verifier that checks trust by `keyId` and cryptography by `publicKey` is
  fully bypassable without this, because the two need never describe the same
  key.
- **A signature does not vouch for its own freshness.** `created` is covered by
  the signature and is therefore not forgeable, but it is chosen by the signer.
  Freshness bounds belong to the protocol; see [protocol.md](protocol.md).

## Key identity

```
keyId = "ed25519:" + base64url( SHA-256( raw 32-byte public key ) )
```

The fingerprint rendered for a human is the hex of that digest in
space-separated groups. It is a display form only and is never parsed back.

## Test vectors

[`../conformance/signing.json`](../conformance/signing.json) publishes a fixed
32-byte seed and the resulting key, key id, fingerprint, canonical payload,
**pre-image as text**, digest and signature.

The pre-image is published as text as well as hex precisely so that an
implementation which disagrees can see _where_ it diverges. A signature
comparison alone tells you only that something is wrong, and the answer is
almost always a canonicalisation difference rather than a cryptographic one.

The reference vectors were verified against an independent Ed25519
implementation, which also confirms that flipping `custody` in the pre-image
makes the published signature fail.
