0010 — signed, release-aware module acquisition¶
Status: DRAFT (design for a second, certified module-acquisition path alongside the existing bring-your-own URL path. Spans two repos — the publisher side is ffmpeg-wasi, the consumer side is afmpeg. Review before building.) Date: 2026-06-29 Parent: 0001-afmpeg.md §3 (module acquisition); 0004 D-0004-C Refines: 0006-hardening-roadmap.md §2F (the "done, with residual" note) Owns: R-AF-14 (certified release acquisition)
1. Why¶
§2F shipped WithModuleURL — fetch by URL, verify a caller-supplied SHA-256, cache. That is the
right primitive for bring-your-own modules (a user hosting their own ffmpeg-wasi build, or a
fork), and it stays exactly as-is: we did not publish those bytes, so there is nothing for us to
certify.
But most consumers take our canonical ffmpeg-wasi releases. For those we can do much better
than "hand-copy a URL and a SHA": we publish checksums.txt and provenance.json with every
release, and we can sign them. A release-aware path lets a consumer say "give me n8.1.2-2
lgpl" and get a module that is checksum-verified, provenance-surfaced, and signature-verified
against a key only our tag pipeline can wield.
Two paths, two postures — this is the core decision:
- By URL (
WithModuleURL) — uncertified. For self-hosted / custom builds. Unchanged. - By release (
WithModuleRelease, new) — certified. For our published artifacts: signature + checksum + provenance, all verified.
2. Decisions¶
- D-0010-A — certification is exclusive to the release path. Provenance and signature checks
apply only to
WithModuleRelease, because they assert facts about our published release.WithModuleURLcarries no provenance/signature (we can't vouch for bytes we didn't publish); its integrity guarantee remains the caller-suppliedWithSHA256. We do not bolt a fake "provenance" onto arbitrary URLs. - D-0010-B — signing mirrors the GTB release chain. Reuse the proven
terraform-aws-signing-kmsmodel: an AWS KMS asymmetric key whose private half never leaves KMS, signable only by the release CI job via OIDC (trust policy scoped to ffmpeg-wasi'sn*tagsub). No human and no long-lived credential can mint a signature. We publish a detached signature overchecksums.txt(checksums.txt.sig), exactly as GTB signs its checksums for self-update. - D-0010-C — the trust root ships in the consumer. afmpeg pins the ffmpeg-wasi release-signing public key (embedded), so verification is offline and non-circular — you never fetch the key you're verifying against. Key rotation ships as an afmpeg release.
- D-0010-D — a dedicated ffmpeg-wasi key, NOT the go-tool-base key. We instantiate a
separate
terraform-aws-signing-kmskey (e.g.ffmpeg-wasi-release-signing-v1) rather than reuse GTB's. We share the infrastructure — the same module, the account's OIDC IDP (terraform-aws-bootstrap), the operator role (terraform-aws-security-baseline) — but not the key. Reasons:- Cross-project signature confusion (decisive). One shared key means any pipeline authorised to sign with it can mint signatures the other project's verifier accepts — same key, same trust root. A compromised ffmpeg-wasi pipeline could forge a go-tool-base self-update (and vice versa). Separate keys give cryptographic domain separation for free; a shared key would force fragile in-payload domain separation that verifiers must remember to enforce.
- Blast radius. A key (or trust-policy) compromise is contained to one product's releases.
- Independent rotation. ffmpeg-wasi can roll its key without forcing a re-pin in GTB consumers, and vice versa.
- Clean provenance. afmpeg pins only the ffmpeg-wasi key, so the GTB key is never a
valid signer for an afmpeg module — "signed by the ffmpeg-wasi release key" is a
self-contained claim.
The module is explicitly built for this — its
ci_subject_filtersguidance is "one tag-pipeline pattern per consuming project," andname/key_specare immutable per instance. A second KMS key costs ~a dollar a month; the isolation does not.
- D-0010-E — raw KMS signature, stdlib verification (resolves Q1). ffmpeg-wasi signs
checksums.txtwithaws kms sign --signing-algorithm RSASSA_PSS_SHA_256, publishing the signature base64-encoded aschecksums.txt.sig. afmpeg verifies with the Go standard library only (crypto/rsa,crypto/sha256,crypto/x509) — no OpenPGP or other third-party crypto dependency. Same KMS key and OIDC gating as GTB; only the envelope differs. Stdlib-only verify is the more auditable surface, and afmpeg's verifier is programmatic (no humangpgstep to serve). - D-0010-F — embedded key-set with overlap rotation (resolves Q2). afmpeg embeds a set
of accepted public keys, each with a stable key-id;
checksums.txt.signames the signing key-id, and verification passes iff that id is in the set and its key validates the signature. Rotation is graceful: mint v2 → ship an afmpeg release adding v2 to the set → switch signing to v2 → drop v1 in a later release once consumers have upgraded. No flag-day; a compromised key is retired by dropping it. The independent WKD second layer is a committed fast-follow — spec 0011 (see §6). - D-0010-G — verification is mandatory (resolves Q3).
WithModuleReleasealways verifies signature + checksum + provenance; there is no skip flag (an opt-out is a silent-downgrade footgun). Verification is offline (the key-set is embedded), so air-gap is served by an offline-bundle mode: pointWithModuleReleaseat a local directory of pre-fetched assets (.wasm,checksums.txt,checksums.txt.sig,provenance.json) and it verifies them fully, no network — certification without exemption. - D-0010-H —
Variantis a typed enum (resolves Q4).type Variant stringwithVariantLGPL/VariantGPL, validated against the known set (unknown → error) and cross-checked againstprovenance.json. A future variant is one new constant. - D-0010-I — hardcoded layout + base override (resolves Q5). The resolver knows the canonical
GitLab generic-package layout and exposes a
WithReleaseBaseURL-style override so a consumer can fetch our release from a mirror / internal store and still verify our signature (the signature is over content, so the URL is untrusted input). A GitLab layout change is a small afmpeg release.
3. Consumer side (afmpeg)¶
3.1 API¶
WithModuleRelease(tag string, variant Variant, opts ...ReleaseOption):
- Resolves the canonical package URL for
(tag, variant)(the GitLab generic-package layout already used in the docs). - Fetches the module,
checksums.txt,checksums.txt.sig, andprovenance.json. - Verifies the signature of
checksums.txtagainst the pinned public key (D-0010-C). - Verifies the module SHA-256 against its
checksums.txtentry. (provenance.jsonis also listed inchecksums.txt, so the one signature transitively certifies the whole asset set.) - Surfaces provenance (ffmpeg version, variant, licence) and asserts the variant matches the request.
- Caches the verified module — reusing the §2F cache (
WithCacheDir,WithHTTPClient,WithGunzipall apply).
New typed errors: ErrSignatureInvalid, ErrProvenanceMismatch (reuse ErrChecksumMismatch).
A new Variant enum (VariantLGPL, VariantGPL). Provenance is exposed (e.g. a Provenance
struct) so a consumer can log/assert exactly what they loaded.
3.2 What stays the same¶
WithModuleURL is untouched — same signature, same WithSHA256 integrity, same cache. The two
options are mutually exclusive (exactly one WithModule* per New, as today).
4. Publisher side (ffmpeg-wasi)¶
- Infra — provision a dedicated signing key + signer role via
terraform-aws-signing-kms(D-0010-D; e.g.name = "ffmpeg-wasi-release-signing-v1"),ci_subject_filtersscoped toproject_path:phpboyscout/ffmpeg-wasi:ref_type:tag:ref:n*. Reuses the shared account infra —terraform-aws-bootstrap(OIDC IDP),terraform-aws-security-baseline(operator role) — but its own key, mirroring thegtb-release-signinginstantiation pattern, not its key. - Release CI (the existing tag-gated
releasejob) — add a GitLabid_tokensOIDC claim, assume the signer role, and signchecksums.txt→checksums.txt.sig. Publish the.sigalongside the other assets (package registry + release links).checksums.txtalready enumerates every asset incl.provenance.json, so signing it certifies the whole release. - Publish the public key once, and pin it into afmpeg (D-0010-C).
Note: ffmpeg-wasi's release job is a custom shell job, not goreleaser, so it signs with a
direct aws kms sign --signing-algorithm RSASSA_PSS_SHA_256 over checksums.txt (D-0010-E) —
no goreleaser signs: block, no gtb signer to run out of context.
5. Trust model¶
The private key lives in KMS and is wieldable only by the ffmpeg-wasi tag pipeline (OIDC
sub match) — not a maintainer, not the apply runner, not a leaked token. The consumer pins the
public key, so a tampered release (swapped module, edited checksums.txt, forged
provenance.json) fails signature verification offline. This is a materially stronger guarantee
than today's "trust the SHA the user pasted."
6. Open questions¶
Resolved (2026-06-29): Q1 signature scheme → D-0010-E (raw KMS RSASSA-PSS, stdlib verify); Q2 key distribution/rotation → D-0010-F (embedded key-set, overlap rotation); Q3 mandatory verify → D-0010-G (always-on + offline-bundle); Q4 variant surface → D-0010-H (typed enum); Q5 tag/URL → D-0010-I (hardcoded layout + base override).
Still open:
- The WKD second layer (→ spec 0011). The KMS signature does not
defend against a compromised GitLab account that can push a tag — that triggers the legit
release pipeline, which signs a malicious build with the real key. Closing this "poisoned well"
needs an independent attestation of release content, rooted in a control plane GitLab cannot
touch (the
phpboyscout.ukdomain). Merely publishing the key via WKD is necessary but not sufficient — the bad release would carry a valid signature — so 0011 must design the layer to attest content (likely an out-of-band/offline signature discovered via WKD), not just the key. A committed fast-follow; sequenced after this spec ships. - Key-id derivation. How a key-id is computed and carried in
checksums.txt.sig(proposed: a SHA-256 fingerprint of the SubjectPublicKeyInfo DER) — pin in implementation. - Rotation cadence. Operational, decided at first rotation; the mechanism (D-0010-F) is set.
7. Non-goals¶
- Signing or "provenance" on the URL path (D-0010-A) — we certify only what we publish.
- A transparency-log / sigstore model — the KMS-pinned-key approach (+ the domain-rooted WKD layer in 0011) is the chosen trust model; we are not adding sigstore on top. (Note: WKD itself is no longer a non-goal — it is the committed second layer, spec 0011.)
- Embedding the module (still — 0001 D-C).
WithModuleReleasefetches+caches; it never//go:embeds a GPL build.
8. Requirements¶
- R-AF-14 — A certified, release-aware acquisition path: given
(tag, variant), afmpeg fetches the canonical ffmpeg-wasi artifact and verifies it against a KMS-backed signature (pinned public key), its published checksum, and its provenance, before it is cached or executed. The bring-your-ownWithModuleURLpath remains uncertified and unchanged.
9. Phasing¶
- Phase 2a — afmpeg verifier (consumer), buildable now. The verification logic is
key-agnostic, so it is built and fully tested before any real KMS key exists: tests
generate an RSA keypair, sign fixture
checksums.txtwith it, and drive the verifier and every tamper case. Delivers theVariant/Provenancetypes, the resolver, the key-set, signature / checksum / provenance verification, the offline-bundle mode, andWithModuleRelease— gated behind a not-yet-populated embedded key-set. - Phase 1 — ffmpeg-wasi (publisher). Signing infra (a dedicated
terraform-aws-signing-kmsinstance, D-0010-D) + release-CIaws kms signofchecksums.txt→checksums.txt.sig+ publish the public key. Needs an AWS apply (operator action), so it runs in parallel with 2a. - Phase 2b — pin the real key. Once Phase 1 publishes the production public key, add it to afmpeg's embedded key-set and cut the first end-to-end verified release.
10. Test & docs strategy (a key deliverable, per the dev method)¶
- TDD, test-first. Every verification rule lands as a failing test first: a valid bundle
passes; each tampered input (swapped module, edited
checksums.txt, forgedprovenance.json, bad/wrong-key signature, unknown key-id, variant mismatch) fails with its own typed error (ErrSignatureInvalid,ErrChecksumMismatch,ErrProvenanceMismatch). Table-driven,t.Parallel(), ≥90% coverage on newpkg/code,go test -raceclean. - BDD acceptance (godog). The trust behaviours are also expressed as Gherkin scenarios — "Given a release signed by a retired key, When I load it, Then it is rejected" — so the security contract reads as executable specification, not just unit assertions.
- Docs land in the same MR (Diátaxis): the obtain-a-module
how-to gains the certified-release path; a reference page documents
WithModuleRelease/Variant/Provenance/ the typed errors; an explanation page covers the trust model (KMS+OIDC, embedded key-set, what each layer does and does not defend — incl. the GitLab- compromise gap that 0011 closes). Packagedoc.goupdated.
11. Definition of done¶
A consumer calls WithModuleRelease("n8.1.2-N", VariantLGPL) and afmpeg loads a module only
after verifying the KMS signature, the checksum, and the provenance — offline against the pinned
key-set — with a clear, typed failure for each tampered case, proven by both unit (TDD) and
Gherkin (BDD) tests at ≥90% coverage. WithModuleURL is unchanged. All three doc types (§10)
ship in the same MRs. The WKD second layer is specced (0011) and sequenced as the fast-follow.