Skip to content

0031 — krites-models: a signed, licence-clean model-provisioning repo

  • Status: IN PROGRESS (2026-07-18) — phase 1 IMPLEMENTED (2026-07-21): krites-models v0.1.0 published (signed, provenance-bearing) and krites' consumer side (pkg/face/models) migrated to verify signature + provenance for the MediaPipe FaceMesh/Blendshapes models. Phases 2–4 pending.
  • Why now: krites depends on six downloaded ONNX models (plus the ORT shared library), each pinned ad-hoc to a third-party host (GitHub raw, Hugging Face re-hosts) with SHA-256 only — no signature, no provenance, no artefact we control. Three carry live licence caveats (§3). LaMa, MI-GAN, the ORT lib and now the MediaPipe models all already carry a "Hardening TODO: mirror to an artefact we control and re-pin" in-code. 0029 shipped the MediaPipe expression models with their registry URL pinned but unpublished, which is the immediate forcing function. This spec resolves all of them at once.
  • Decision (2026-07-18): stand up a dedicated krites-models repo that fetches → converts → parity-verifiessigns → publishes each model as a release asset we control, and migrate krites' consumer side to verify the signature + provenance, not just a checksum. Model directly on the proven phpboyscout/ffmpeg-wasi (producer) + afmpeg (consumer) chain rather than inventing one — same signing backend, OIDC role pattern, package-registry layout, and gitlab.com/phpboyscout/go/signing/verify library.
  • Realises: the 0029 §7 hosting follow-up; the LaMa/MI-GAN ("remove/onnx/models.go") and ORT (pkg/onnxruntime) hardening TODOs; the 0001 §5 local-first / provenance posture.
  • Depends on: 0013 (ORT provisioning), 0004/0029 (the models), the phpboyscout signing chain (go-tool-base aws-kms backend, phpboyscout/go/ signing, phpboyscout/cicd components). Cross-references: the Places2 licence risk tracked for 0008.

1. The problem

pkg/face/models, pkg/remove/onnx/models, and pkg/onnxruntime each pin an artefact by {URL, SHA-256} and download-on-first-use. That gives integrity (a mismatched download is refused) but not:

  • Provenance / authenticity — nothing proves the artefact is the one we produced from a known source; we trust GitHub/HF to serve the bytes the hash expects, and if the upstream URL 404s or is replaced, provisioning breaks.
  • Control — several pins are someone else's re-host (the Carve LaMa export, the licence-less MI-GAN re-host), which can vanish or change.
  • A publishable MediaPipe pin0029's models are converted by us and hosted nowhere yet; the strategy only works from a warm cache.
  • A single place to state each model's licence + provenance — today it is scattered across three Go files' doc comments.

2. Shape — mirror ffmpeg-wasi / afmpeg

ffmpeg-wasi already solves this exact class (disparate licences, an artefact we build + sign, a consumer that verifies): tag-driven releases, a KMS-held OpenPGP key assumed via GitLab OIDC (reproducible fingerprint), assets + checksums.txt + checksums.txt.sig + provenance.json + release.asc published to the GitLab generic package registry, and a consumer (afmpeg) that pins the URL + a vendored public key and verifies the detached signature, the per-asset SHA-256, and the provenance via gitlab.com/phpboyscout/go/signing/verify. We adopt that chain wholesale; the only krites-specific differences are in §5.

krites-models repo (new)
  pipeline (tag-gated):  fetch upstream ─▶ convert/curate ─▶ parity-verify ─▶ sign ─▶ release
                           │                  │                  │             │        │
                     versioned source   tf2onnx / passthrough  golden values  KMS+OIDC  generic
                     recorded in                               reproduced     OpenPGP   package
                     provenance.json                           (not a hash!)  detached  registry
                                                                              sig
krites (consumer):  pkg/face/models + pkg/remove/onnx  ─▶  verify sig + SHA-256 + provenance
                    (adopt phpboyscout/go/signing/verify; vendor the krites-models pubkey)

3. Model inventory & licence audit (the reason to centralise)

Model Role Source & how Licence Commercial track
MediaPipe FaceMesh expression landmarks (0029) Google face_landmarker.taskconvert (tf2onnx) Apache-2.0, Google-trained ✅ clean
MediaPipe Blendshapes expression coeffs (0029) same bundle → convert Apache-2.0, Google-trained ✅ clean
UltraFace RFB-320 face detect (0004) ONNX Model Zoo → mirror MIT (Linzaer) ✅ clean
InsightFace 2d106det eye/facing landmarks (0004) InsightFace buffalo_lmirror ⚠️ non-commercial / research only blocker
LaMa (big-lama fp32) inpaint (0008) Carve HF export → mirror Apache code, Places2 non-commercial training data ⚠️ unsettled
MI-GAN pipeline v2 inpaint (0008) licence-less HF re-host → mirror MIT code, Places2 training data ⚠️ unsettled
(ORT shared library) inference runtime (0013) Microsoft GH release → mirror (binary, no convert) MIT ✅ clean (in scope as mirror-only)

Centralising makes this table enforced, not folklore: each release's provenance.json records the model's upstream source, version, and licence, and the consumer surfaces it. Two clean-up paths this exposes:

  • Drop the non-commercial 2d106det by moving eyes/facing onto the MediaPipe FaceMesh we already ship — 0029 phase 4 (shared backbone). DECIDED (2026-07-18): do it — make mediapipe the primary/default face strategy and keep ear + 2d106det as an opt-in fallback, leaving the entire default face stack Apache-2.0/MIT. Tracked in 0029 (promoted); gated on this repo hosting the models first (a fresh default install must have something to download).
  • Replace the Places2-trained inpainters with a permissively-trained model — the standing Places2-licence-risk item (still future).

Until those land, the tainted models stay fine for private, self-hosted use and are published with an explicit licence record + disclosure — the same good-faith posture we already hold, now in one auditable place.

4. The krites-models repo

Per-model pipeline, tag-gated (a release is cut by tagging), following ffmpeg-wasi's validate → build → sign → release stages:

  1. Fetch the pinned upstream (URL + its own SHA recorded in provenance).
  2. Convert or curate. MediaPipe: tf2onnx opset-17 (the existing scripts/convert-mediapipe-models.py, moved here). Mirror-only models (UltraFace, 2d106det, LaMa, MI-GAN, ORT lib): pass through unchanged.
  3. Parity-verify — the load-bearing gate. tf2onnx output is not byte-reproducible (0029: functionally identical, different SHA every run), so we cannot pin on a rebuild hash. Instead the pipeline runs the converted model against frozen golden reference values (e.g. the bride smile 0.8141) and fails the release if it drifts. Mirror-only models verify the fetched bytes against their recorded upstream hash. This gate is what makes an unattended re-convert safe.
  4. Sign — with a dedicated krites-models key (domain-separated). A checksums.txt over all assets + a detached OpenPGP signature (checksums.txt.sig) via gtb sign --backend aws-kms, the KMS key assumed through GitLab OIDC (no static creds). Provision a new dedicated key + signer role via the reusable signing-kms/aws module — krites-models-release- signing-v1, OIDC-filter-locked to project_path:phpboyscout/krites-models: ref_type:tag:ref:v* — mirroring how ffmpeg-wasi has its own key. This preserves the D-0010-D domain-separation decision (each project's tag pipeline can mint only its own project's signatures — a compromise of one cannot forge another's). It reuses the shared signing infrastructure (the GitLab OIDC IDP, the <project>-automation apply role, break-glass admins), only the key + signer role are new (a single module block in infra/src/ main.signing-kms.tf). Emit provenance.json (per-model upstream source/version/licence). Sign all assets — converted, mirror-only, and the ORT binary mirror alike (uniform trust chain).
  5. Release to the GitLab generic package registry (…/packages/generic/krites-models/<tag>/…) — one release, many assets, under a single checksums.txt/signature with provenance.json carrying the per-model records.

Versioning / upstream tracking. Tag-driven and deliberately human-initiated (ffmpeg-wasi's model): a tag encodes the release, provenance.json records each model's exact upstream version. Renovate on the repo (renovate-self + the shared gitlab>phpboyscout/cicd preset) keeps the repo's own CI/deps current; adopting a new upstream model is a chosen tag, not an auto-merge — a model swap deserves review. (Upstream model feeds are not clean semver sources, so auto-datasource tracking is out; a tag is simpler and safer.)

5. Consumer side (krites)

Migrate pkg/face/models (the shared fetcher) to the ffmpeg-wasi/afmpeg trust bar:

  • Repoint every Model.URL at the krites-models generic-package release.
  • Add signature + provenance verification on top of the existing SHA-256: verify the detached sig over checksums.txt, then the asset SHA against the manifest, then assert provenance.json (surfacing the licence for the disclosure UI/logs). A failure at any step refuses the artefact — never a silently-used file (R-MLR-4).
  • Trust anchor — vendor the krites-models public key (its own .asc in the krites tree) and pin its fingerprint, exactly as afmpeg vendors ffmpeg-wasi's key. Verify with gitlab.com/phpboyscout/go/signing/verify (or gtb's pkg/signing). The key is dedicated (D-0010-D), so it is not gtb's binary-signing anchor — a distinct trust root for the models, exported from KMS once at provisioning time via gtb keys export / kms:GetPublicKey.
  • pkg/remove/onnx/models and pkg/onnxruntime move onto the same fetch + verify path, retiring their in-code hardening TODOs.
  • Golden fixtures stay the drift guard. krites' own pkg/face/onnx/testdata parity test still runs; a model release that changed behaviour would break it and force human review of the Renovate pin-bump MR — the deliberate guardrail.

The pin-bump flow mirrors the go-tool-base bumps already seen: krites-models cuts a release → Renovate opens an MR in krites bumping the pinned URL + SHA (+ fingerprint only if the key rotated).

6. Phasing

  1. Repo + MediaPipe + signing chain. ✅ DONE (2026-07-21). Stood up krites-models, ported the conversion script, wired the KMS/OIDC OpenPGP sign
  2. generic-package release, published the two 0029 models (v0.1.0). Migrated pkg/face/models to verify sig + provenance and repointed FaceMesh/Blendshapes at the release — an embedded, fingerprint-pinned (5B648A6B…) trust key verifies the detached signature over checksums.txt, the in-code SHA pins bind to that signed manifest, and provenance.json is asserted (Apache-2.0/clean) before any model byte is trusted. Third-party pins (UltraFace, 2d106det, LaMa, MI-GAN) keep the plain checksum fetch until phase 2. Closes the 0029 hosting gap. Note: provenance.json is not itself listed in checksums.txt in v0.1.0, so the consumer cross-checks its per-asset SHA against the signed manifest; a producer-side follow-up should add it to checksums.txt for a direct signed-checksum verification.
  3. Fold in the mirror-only models. UltraFace, 2d106det, LaMa, MI-GAN — fetch, verify-vs-upstream, sign, publish; repoint krites; per-model provenance.json licence records + disclosure. Retire the in-code hardening TODOs.
  4. ORT shared library. Mirror the Microsoft archive(s) the same way (pkg/onnxruntime), retiring its TODO. Binary mirror, no conversion.
  5. (Later, tracked elsewhere) licence clean-up. Drop 2d106det via the 0029 phase-4 FaceMesh backbone; replace the Places2 inpainters. Not this spec — this spec makes the debt visible and centrally managed.

7. Open questions

  • Q1 — repo scope/name. krites-models vs a generic phpboyscout/onnx-models? RESOLVED (2026-07-18): krites-models. Generalise only if a second tool needs it.
  • Q2 — one release with all models, or per-model releases? one vs per-model RESOLVED (2026-07-18): one release, many assets — single checksums.txt/ signature, provenance.json carrying per-model records.
  • Q3 — sign the ORT binary mirror too, or checksum-only? sign vs checksum RESOLVED (2026-07-18): sign everything, the ORT binary mirror included.
  • Q4 — signing key. RESOLVED (2026-07-18): a dedicated krites-models key. Investigating ../infra showed there is no group-scoped key — the gtb and ffmpeg-wasi keys are each OIDC-filter-locked to one project, by the deliberate D-0010-D domain-separation decision. So krites-models gets its own krites-models-release-signing-v1 key + signer role via the signing-kms/aws module (one new block in infra/src/main.signing-kms.tf), locked to phpboyscout/krites-models tag pipelines; krites vendors + pins that key. This preserves D-0010-D rather than widening the gtb key to the group. (Superseded earlier drafts that proposed reusing the gtb key — that would reverse D-0010-D.)

8. Non-goals

  • Not re-training or fine-tuning any model (that is the Phase-4 learner / future permissive-training work).
  • Not resolving the InsightFace non-commercial or Places2 training-data licence questions — this spec records and centralises them and points at the replacement paths; the legal calls stay separate.
  • Not bundling models into the krites binary — download-on-demand + verify stays the model (size, updatability, consistency with the ORT lib), even though the Apache-2.0/MIT models could legally be bundled.
  • Not changing any inference/behaviour — pure provisioning/provenance.