# 0193 — An unchecked signature must be visible

> Source: https://github.com/max-listov/stitchkit/blob/master/docs/decisions/0193-an-unchecked-signature-must-be-visible.md

---
# 0193 — An unchecked signature must be visible

**Status:** Accepted
**Date:** 2026-09-22

## Context

The distribution primitive proved delivery and could not prove authorship. An
asset digest says the bytes that arrived are the bytes the manifest named; the
manifest and the assets come from one origin, so whoever replaces one replaces
the other. Worse, the manifest schema was an ordinary object and stripped what
it did not know — a signature published in the very same document reached
nobody, and a signed install had to fetch that document a second time and parse
it twice to see its own proof.

## Decision

`signature` is part of the schema. Ed25519 over `{name, version, commit,
builtAt, assets[]}`, every asset contributing its target, compression, size and
digest.

**What is signed is everything that acts before the digest can disagree.**
`url` is deliberately out: where a file is served from is the publisher's
business, and moving it must not invalidate the proof of what it contains.
`compression` is deliberately in, and this was nearly missed: it decides how the
transferred bytes are expanded, expansion happens before any digest check, and
one unsigned byte turns a small download into a large allocation with the
signature still valid. The expansion is additionally bounded by the signed
`size`.

**Five verdicts, and `unenforced` is the one that earns its keep.** A build with
no pinned key must keep updating, and the fact that nothing was checked has to
be *visible* rather than assumed. A silent "fine" from a check that never ran is
the failure this whole file exists to remove. `missing`, `unknown-key` and
`invalid` are distinguished because they call for different answers: a key
someone forgot to pin is not a forged signature.

**A bad verdict is not a fifth status.** `outdated` is an instruction to
install, and a manifest that failed its signature has not established that there
is a newer build worth installing — only that a document claims one. The check
answers `unknown` with the verdict in its reason, which keeps every consumer's
exhaustive `switch` compiling and never reads as a network failure.

## Consequences

**The channel is not an argument of the framework, and will not become one.**
The consuming project raised it and withdrew it after reading the installer
renderer, and they were right: the framework has no model of how URLs are built
and should not acquire one. A channel is two documents at two addresses.
`assertCliPublishable` is therefore told about *all* of them rather than taught
the shape of any — with one argument and two tracks it is silently useless, and
the person who installed the beta is told they are current forever.

`applyCliUpdate` needs the manifest when given a trust root, because an asset
alone carries nothing to verify. That is a signature change, named in the
migration.

Nothing was adopted from the consuming project's own implementation: that file
is not in this tree and was not read. Their measurements of it remain theirs.

One shared primitive came out of this — `writeFileAtomic` — and it is used by
the binary replacement, the backup and the batch checkpoint. The eight other
places in `src` with their own staged-plus-rename were **not** migrated in this
pass; that is a separate change and doing it here would have meant touching
unrelated scope. The primitive stages under a random name and creates
exclusively, because a name built from pid and clock is guessable and an
ordinary write follows a symlink: anyone able to write to the directory could
otherwise redirect a binary replacement through a planted link.
