# Trust documents v1

Status: draft, implemented by mago-ski 0.1 and later. Test vectors: [`tests/vectors/`](../../tests/vectors/).

An organization's trust is anchored in one **root key**, kept offline. Hosts pin the root key's fingerprint (its key id). The root signs two documents: the trust root (who may approve what) and the revocation list (what is no longer trusted). Both are DSSE envelopes signed by the root key, in the same envelope format as [certificates](skill-certificate-v1.md), with canonical JSON payloads.

## Trust root

`payloadType`: `application/vnd.mago-ski.trust-root+json`

```json
{
  "type": "mago.trust-root/v1",
  "version": 3,
  "issued_at": "2026-10-09T12:00:00Z",
  "expires_at": "2027-10-09T12:00:00Z",
  "root": { "key_id": "sha256:<hex>", "public_key": "<base64 DER SPKI>" },
  "approvers": [
    { "key_id": "sha256:<hex>", "public_key": "<base64 DER SPKI>", "name": "Platform reviewers",
      "scopes": ["docs-*", "pdf-tools"], "expires_at": "2027-04-07T12:00:00Z" }
  ]
}
```

- `version`: positive integer, increased on every change. Hosts with a configured state file reject versions lower than those recorded there.
- `root.public_key` must hash to `root.key_id`, which must equal the pinned fingerprint and the envelope's `keyid`.
- `approvers`: sorted by `key_id`, unique, at most 256. The root key may not also be an approver. With root recovery, a host also refuses (`UNVERIFIABLE`) a trust root that lists an approver whose key has held the root or timestamp role in the root events the host walked: `trust root lists approver <id>, which has held the root or timestamp role; a key that left those roles never takes another`. Only keys retired in events after `root_events_after` are known, and the gate trust store does not check it. Before upgrading, remove such approvers (see the CHANGELOG).
- `scopes`: sorted, unique, 1–64 per approver. `*` matches every Skill name; `prefix*` matches names starting with `prefix`; anything else matches one exact name.

## Revocation list

`payloadType`: `application/vnd.mago-ski.revocations+json`

```json
{
  "type": "mago.revocations/v1",
  "version": 2,
  "issued_at": "2026-10-09T12:00:00Z",
  "expires_at": "2026-11-08T12:00:00Z",
  "root_key_id": "sha256:<hex>",
  "revoked_keys": [{ "key_id": "sha256:<hex>", "reason": "laptop lost", "revoked_at": "2026-10-09T12:00:00Z" }],
  "revoked_digests": [{ "digest": "sha256:<hex>", "reason": "exfiltration found", "revoked_at": "2026-10-09T12:00:00Z" }]
}
```

- Required: a host with no valid, unexpired revocation list verifies nothing. The short default lifetime (30 days) forces the organization to republish it, so a host cannot be kept on a stale list indefinitely.
- A revoked key invalidates every certificate it signed. A revoked digest invalidates that approved version whoever signed it.
- `revoked_keys` sorted by `key_id`, `revoked_digests` by `digest`, both unique.

## Host policy

Hand-edited JSON, read by hosts and the CLI. Paths are relative to the policy file.

```json
{
  "policy_version": "mago.policy/v1",
  "mode": "shadow",
  "root_fingerprint": "sha256:<hex>",
  "trust_root": "trust-root.json",
  "revocations": "revocations.json",
  "certificate_dirs": ["certs"],
  "skill_dirs": ["../skills"],
  "state_file": "state.json",
  "decision_log": "decisions.jsonl"
}
```

| Field | Required | Meaning |
|---|---|---|
| `mode` | yes | `shadow`: allow and log what would be blocked. `enforce`: block |
| `root_fingerprint` | yes | Pinned root key id |
| `trust_root`, `revocations` | yes | Signed documents |
| `certificate_dirs` | no | Extra certificate locations |
| `skill_dirs` | no | Directories scanned by `verify-all` and the GitHub Action |
| `state_file` | no | Rollback protection (below). Strongly recommended on hosts. Must resolve to a file inside the policy file's directory |
| `decision_log` | no | JSON Lines file of decisions, one JSON object per line. Must resolve to a file inside the policy file's directory (after following symbolic links); it is opened without following symbolic links and must be a regular file. Records are data, not shell syntax: do not source them |

Policy `mago.policy/v2` adds `recovery`, `root_events`, `root_events_after` and `freshness` for root recovery; see [Host policy v2](root-recovery-v1.md#host-policy-v2). Without `recovery`, a v2 policy behaves exactly as v1. `root_events` and `freshness.timestamp` are inputs too: `state_file` and `decision_log` must not be either of them.

The policy itself decides who is trusted, so it must live where the code under review cannot change it. The Pi host and the CLI read it only from `MAGO_SKI_POLICY` (an absolute path) or `~/.mago-ski/policy.json`, and the CLI also accepts an explicit `--policy FILE`; the GitHub Action reads it from the pull request's base revision.

## Host state (rollback protection)

Canonical JSON written by the host:

```json
{"roots":{"sha256:<root key id>":{"revocations_version":2,"trust_root_version":3}},"type":"mago.host-state/v1"}
```

The host records the highest versions it has accepted for each root and rejects older documents as `UNVERIFIABLE` (rollback). Without a state file, a host can be given an older, still-unexpired trust root or revocation list.

With root recovery, the host writes `mago.host-state/v2`, which adds the root event progress per pinned root and the timestamp version per timestamp key ([Rollback and forks](root-recovery-v1.md#rollback-and-forks)). A host keeps writing v1 until it applies its first root event or timestamp, and never goes back to v1 once it has written v2.

## Decision log

One JSON object per line:

```json
{"timestamp":"2026-10-09T12:00:00Z","host":"laptop-7","mode":"enforce","action":"block","event":"tool:read","skill":"pdf-tools","skill_dir":"/home/a/.agents/skills/pdf-tools","digest":"sha256:...","certificate_id":"sha256:...","verdict":"UNAPPROVED_CHANGE","reason":"..."}
```

`action` is `allow`, `block` or `would-block` (shadow mode). `certificate_id` is the SHA-256 of the certificate file.

A failure of the trust documents (trust root, revocation list, root event log or timestamp) affects every Skill, so hosts write **one** record when it starts or changes, instead of one per Skill: `event` is `root-revoked` or `trust-unavailable`, `skill` and `skill_dir` are empty, `digest` and `certificate_id` are null, and `verdict` is `UNVERIFIABLE`. `root-revoked` is always `block`, in shadow mode too ([Revoked root](root-recovery-v1.md#revoked-root-blocked-in-every-mode)).

## Key rotation

- **Approver keys:** add the new key (`root add-approver`), re-approve the Skills, then remove the old key (`root remove-approver`). Certificates from a removed approver stop verifying once a host reads the updated trust root; revoke the old key instead if it may be compromised.
- **Root key:** with a v2 policy and a recovery set, the root key is revoked and replaced by root events signed by recovery keys, without editing host policies: [Root recovery v1](root-recovery-v1.md). After a rotate, `mago-ski root adopt` re-signs the trust documents with the new root. With a v1 policy, replacing the root still means distributing a new fingerprint to every host.
