Revised 2026-09-20, after pipelines/tasks/0071. This ticket no longer signs
a binary. It signs an OperationBundle: one or more store.Operation
declarations (and, once SAPE-14 lands, the collections they need), plus a
readable Budget, hashed as one unit and signed with ed25519. An external
operation arrives as data from somebody who is not the operator; sign the bundle at publish time and
verify it at install time, offline, before the server that installs it has started, so that installing
succeeds only for the exact bytes that were signed.
Installing a stranger's declarations is still a privileged act — it puts their vocabulary into a database an operator runs — and the moment that is possible without verification is the moment the answer to "where did this come from" is "somewhere". It is also a public-surface decision: the bundle format, the signature, and where the trusted-signer list lives travel with every operation anybody publishes, and changing them after strangers have signed things invalidates their work.
Measured 2026-09-20, and this is the finding that rewrote the ticket. There is still no binary-loading path — searching forplugin.Open,wasmand.soacross the repository returns no non-test hits. Butpipelines/tasks/0071measured, in a lab outside this repository, that a binary was never the right target for the signature to protect in the first place: an EO built as arbitrary code with read access — WASM, sandboxed, deadline-limited, signed with a real key — declared a scan limit of 50 in its manifest and served 50,000,000 rows in one call, because the honest path and the greedy path sat in the same signed binary. A signature proves who wrote something. It proves nothing about what it costs. Any design that asks a signature to carry the cost promise is wrong from its first line — which is why 0071 recommends v1 ship no external code at all: anOperationBundleof declarations, whose cost isceiling()(internal/store/compose.go:99), enforced at declare time by code that already exists, never by the signature.
And the existing crypto still does not cover even the narrower job of proving who signed the bundle:internal/signing/signing.goimports exactlycrypto/hmacandcrypto/sha256and nothing else — every one of its three signing paths (Signat:107,Operatingat:159, the scope grant at:331) ishmac.New(sha256.New, …)over a shared secret. A repository-widegrepfored25519,crypto/rsaorcrypto/ecdsamatches exactly one line outside this search itself —internal/service/service_test.go:5, which usescrypto/ecdsato build a throwaway TLS certificate for a test, not a signing path. HMAC proves the signer held the server's secret. A bundle author is precisely somebody who must not hold it, so verifying a third party's bundle needs a primitive this package does not have.
| Thing | Decision | Why |
|---|---|---|
| Algorithm | crypto/ed25519 — Go standard library | No new dependency. A 32-byte public key and a 64-byte signature, deterministic verification, nothing to configure wrong. |
| Who holds what | The bundle author keeps the private key and never ships it. The admin keeps
trusted_signers: a list of public keys (32 bytes each) plus a name. The server holds no
bundle-author key at all. | This is the exact inversion of HMAC's failure: the author proves an identity without holding anything of the server's. |
| Verification | sapedb verify <bundle> — offline, three checks: the
ed25519 signature covers a hash of the whole bundle; the signing public key is in
trusted_signers; the declared Budget is printed verbatim for a human to
read. | All three are answerable before the server that installs the bundle has even started, and none of them makes a network call. |
| sigstore / cosign | Not for v1. | Keyless verification needs a transparency log reachable at verification time. A database that cannot start because somebody else's log is down is a worse product than the one this ticket is fixing. Not measured, only argued — see 0071 §8. |
Proposed identifiers (English, because they become the product's vocabulary):
OperationBundle, bundle.json, TrustedSigner,
trusted_signers, Budget, ErrUntrustedSigner,
ErrBudgetExceeded, ErrBundleShape.
trusted_signers.
Two separate cases, both asserted, because they fail for different reasons and one can pass while the
other silently does not.sapedb verify runs with no network access available, and still completes all three
checks. Asserted by running it with outbound networking disabled.trusted_signers list refuses every bundle, including one signed by a
key that will later be trusted — fail-closed, not fail-open. This is mutation M1 from
0071 §9, and the refusal message says explicitly that an empty list means nobody is trusted, so it is
never misread as "trust everyone".KindDeclaration bundle runs no code of the author's —
see the blockquote — so there is nothing to sandbox in v1. If SAPE-8's
KindTransform cousin is ever built, its sandbox is a separate ticket, not this one.internal/signing as-is. It signs with the server's own secret, which is the
wrong shape for a third party, and widening it would weaken the three things it already does.