# Signed update metadata trust

Reckon's update trust is local and channel-independent. npm, Homebrew,
Artifactory, GitHub SaaS, and GHES may transport metadata and artifacts, but no
one transport is an authority to install code.

`src/update-trust.ts` is the pure verification boundary. It performs no network
request, package-manager command, filesystem activation, or telemetry write.
`src/update-policy.ts` composes that verifier with bounded HTTPS fetching,
private monotonic state, policy, channel cross-checking, quarantine, exact local
manager artifacts, health validation, and rollback. C64 adds the release-time
CycloneDX/Sigstore evidence and fail-closed five-channel attestation machinery;
C65 must still certify the smoke sandbox and recovery drills, and a real release
must produce verified evidence before activation is enabled.

## Trust model

The metadata model has four disjoint Ed25519 roles:

| Role | Purpose | Maximum signed lifetime | Required threshold |
| --- | --- | ---: | ---: |
| `root` | Delegates keys and rotates trust | 366 days | At least 2 |
| `targets` | Authorizes immutable release artifacts and channels | 93 days | At least 2 |
| `snapshot` | Binds an exact targets version, length, and digest | 32 days | At least 1 |
| `timestamp` | Publishes the current snapshot and bounds freezes | 8 days | At least 1 |

Root and targets thresholds prevent one stolen signing key from authorizing a
release. All four roles must use separate, content-addressed keys. A root
rotation is accepted one version at a time and only when both the previously
trusted root threshold and the candidate root threshold sign the new root.

The initial root envelope is trusted only when its SHA-256 matches an
independently pinned digest shipped by a previously trusted Reckon build. The
pin must never be learned from fetched metadata or a package registry.

Metadata is signed over deterministic UTF-8 JSON: object keys use ECMAScript
code-unit ordering, arrays retain order, and undefined, cyclic, non-finite, or
non-plain values are rejected. Key IDs are the SHA-256 of the canonical public
key record.

## Verification order

The verifier fails closed in this order:

1. Validate the installed trusted root and its independent digest pin.
2. Verify the trusted root's own threshold signatures.
3. Accept the same root byte-for-byte, or verify a one-step rotation under both
   old and new root thresholds.
4. Validate role separation, schema, generation time, expiry, and maximum
   metadata lifetime.
5. Verify threshold signatures for targets, snapshot, and timestamp.
6. Bind timestamp to the exact canonical snapshot envelope by version, byte
   length, and SHA-256.
7. Bind snapshot to the exact canonical targets envelope the same way.
8. Compare every role's version and digest with durable prior state. Lower
   versions are rollback; changed bytes at the same version are equivocation.
9. Select only the requested relative target path.
10. Before quarantine, compare the downloaded artifact with its signed byte
    length and SHA-256.

The successful result returns the exact verified root envelope. C63 persists it
for evidence, but never promotes a root merely because mutable local state says
it is trusted: each check and rollback remains anchored to the independently
shipped initial root and pin. When more than one rotation was missed, the client
fetches each numbered intermediate root, bounds one check to 32 rotations,
cross-checks every intermediate across configured mirrors, and advances only
one dual-threshold version at a time. A skipped, substituted, malformed, or
wrong-version intermediate root fails closed without persisting new trust.

The signed target also carries the semantic version, release channel, full
source commit, and digests for build provenance, the CycloneDX SBOM, its signed
attestation, and the cross-channel release attestation. Metadata validation
accepts these fields so notify-only clients can read a staged repository, but
download/apply refuses when any digest is absent. The verifier does not invent
missing provenance or promote a merely claimed channel result.

## Defenses and limits

Executable tests cover valid threshold authorization, missing threshold
signatures, signed-field mutation, artifact substitution, expired timestamp
freeze, rollback, same-version equivocation, mix-and-match metadata, role-key
reuse, root-pin substitution, future metadata, and root rotation without the
old threshold. Recovery tests additionally cover missed sequential rotations,
skipped roots, intermediate mirror disagreement, and malicious version jumps.

The verifier cannot make the first installation trustworthy by itself. A
production release still requires an offline root-key ceremony, independent
pin distribution, durable trusted-state storage, compromised-key rotation
drills, and verified artifacts on every approved channel. Until that ceremony
and C64–C65 are complete:

- the former npm `latest` hint is not used by doctor, status, MCP, or update commands;
- no release is authorized by `src/update-trust.ts` in production;
- no automatic update may be enabled; and
- a registry, formula, release page, or mutable URL must not be treated as a
  root of trust.

## Production key ceremony gate

Private root keys must be created and retained offline under separate
custodians. No production private key or recovery secret belongs in this
repository, CI variables, npm, Homebrew, GitHub, GHES, or Artifactory. Before
activating signed checks, the release owners must record:

- custodians and a two-person approval path;
- public key IDs and the independently reviewed root-envelope digest;
- offline backup and recovery controls;
- online targets/snapshot/timestamp key storage and rotation periods;
- emergency revocation and root-rotation rehearsal evidence; and
- the exact Reckon source commit that first embeds the trusted root pin.

This external ceremony is deliberately a release gate rather than generated
test material masquerading as production trust.

## Client policy and recovery

The implemented policy modes are `notify-only`, `download-verify-only`,
`apply-patch`, and `apply-minor`; major updates always require manual approval.
Policies also bound release age, approved channels, enterprise mirror origins,
optional exact metadata/artifact cross-checks, and UTC maintenance windows.

`reckon update status` and `explain` are offline reads. `check` uses no
repository context. `apply` first quarantines and verifies the candidate plus a
signed artifact for the currently installed version. A missing rollback target,
provenance object, SBOM, release attestation, smoke sandbox, manager adapter, or
health check is a refusal, not a warning. Explicit rollback re-verifies the
stored signed metadata, artifact, provenance, SBOM, both attestations, and every
quarantine path before invoking the manager; nested symlink redirection is a
hard refusal. Apply also requires the candidate attestation's rollback version,
path, and derived digest to match the separately downloaded and verified rollback
artifact. Signed
non-SemVer target versions are ineligible before path construction; quarantine
uses fresh exclusive files under a symlink-rejecting private root; and automatic
rollback succeeds only after a second health check validates the restored
installation.
