← 1.0.0 scope SAPE-28

Check a signed bundle, then install what it declares — the half of SAPE-10 that was still a library

Status
Done
Version
1.0.0
Component
server
Blocked by
Blocks
SAPE-12
Commit
bce1c17 on main

Why this ticket exists at all

SAPE-10 taught the codebase to seal and verify an operation bundle. It did that inside internal/bundle, where only Go code can reach it. Nothing in the shipped binary could act on a bundle, so the signing work was real and entirely unreachable — an operator handed a signed bundle had no command to run.

That is the gap SAPE-12 kept walking into. A worked external operation, published as a challenge to a stranger, needs a stranger-usable path from file to installed. This ticket is that path.

What shipped

Two commands. sapedb verify FILE is standalone: no database, no lock, no secret, no network. It prints four rows and refuses each of them separately, so an operator can tell never signed from signed, then changed from signed by a stranger — three situations that demand three different responses and that a single "invalid bundle" would have flattened into one.

  read      yes  sapedb/bundle:v1
  signed    yes  ed25519, key 66be7e33…0c473a
  intact    no   these are not the declarations that key signed
  trusted   -    not reached; the signature did not check out

The report is printed on refusal too, and it prints what the bundle declares under a heading that says nothing below it is vouched for. A reader deciding whether a file is worth chasing the author about needs to see the contents even when — especially when — the checks failed.

It also prints two names side by side, which is the part worth slowing down for:

  says it is "Ledger Authors" (the bundle's own claim about its author)
  known as  "north-star" (what an operator of this server called that key)

The first is signed but self-asserted: a bundle can claim any author it likes. The second is what somebody on this server wrote next to that key. Showing only the first would let a bundle name itself trustworthy.

sapedb install FILE runs the whole verification before it opens anything, so a bundle nobody trusts leaves no .lock, no account folder and no empty database behind. Five tests assert the directory is byte-for-byte unchanged after a refusal.

Trust, and the spelling that does not exist

An operator names trusted signers in one environment variable, SAPEDB_TRUST, as label=key entries separated by a comma or a newline — a shell writes one line, a systemd EnvironmentFile may not, and an operator should not have to know which the parser wanted.

There is no spelling of that variable that means "trust anything." The only shape the parser accepts is 64 lower-case hex characters, so *, any=any and everyone=* are each either an empty list or a refusal — and an empty list refuses every bundle rather than permitting one. A test pins exactly that, by name. A malformed list is refused rather than treated as empty, because an operator who mistyped a key must not be sent off to write a list they already wrote.

One label over two keys is refused as well. The label is what verify prints and what the change log records; one label naming two keys makes both of those say something untrue.

What the change log records

1 declare   | bundle "ledger-pack" "2.1.0" signed by 66be7e33…0c473a, trusted here as "north-star"

Not the account that ran the command. Four parts, each answering a different question: the key in full (the identity, and the only fact on this path that anything actually checked — truncated, it could not be compared against a trust list), the operator's label at the time (a later rename of the trust list does not rewrite history, which is correct), and the bundle's own name and version (self-asserted, signed, and what an author will quote back at you).

The measurement worth keeping

A refused install rolls back explicitly instead of leaning on the process exiting and the pager closing an uncommitted transaction. That distinction sounds academic until you notice that apply() can lean on it — apply() is the process — while a caller holding the same open store afterwards would be handed collections that are in the tree and will never be committed.

The load-bearing part: the process-level test stays green when that rollback is deleted. One test would therefore have been a guard nobody had ever watched fail. So there are two, and the second one exists only because the first was measured to be blind to this.

A note on the ticket number

This work came back labelled SAPE-11, which is a different ticket that had already shipped. The label was corrected before the merge and the number allocated here. It is a small thing, and it is the same family as everything else on this board: a name is not a measurement, and the only way to find out which ticket a diff belongs to is to read the diff.

Acceptance, as measured

  1. go build ./..., go vet ./... and gofmt -l . clean on the merged tree.
  2. go test ./... -count=1 green: all 20 packages ok.
  3. go test ./... -race -count=1 exits 0 with no FAIL.
  4. 8321 passing tests counted as --- PASS lines including subtests, on bce1c17 with a clean tree.
  5. Every new guard was watched failing from a deliberately broken copy — twelve of them, each with its red output recorded — and the copy reverted.

What it does not do