Files
shopdb-flask/docs/PLUGIN-SIGNING.md
cproudlock 86f5f1be68 ADR-013 Phase 1: signed plugin artifacts (pack/validate/keygen)
Packaging + provenance for the plugin marketplace. No runtime behavior change
yet - verification is available on demand; enforcing it at plugin load/migrate
and pulling from a shelf are Phase 2.

- signing.py: ed25519 key pairs + provenance. Provenance is a sorted per-file
  SHA-256 map plus metadata; the detached signature covers the exact
  serialized provenance bytes, so verifying is re-hash files, re-serialize,
  check signature. verify() accepts any of several trusted keys (rotation).
  Uses cryptography (already a dependency).
- packaging.py: pack() builds a signed <name>-<version>.shopdbplugin (zip +
  PROVENANCE.json + PROVENANCE.sig). verify_artifact()/verify_dir() re-hash
  and check the signature, and flag a tampered file, an unexpected file, a
  wrong/absent key - all fail closed.
- CLI: `flask plugin keygen` (publisher key pair), `flask plugin pack <name>
  --key` (validates then signs), and `flask plugin validate` extended to a
  signed artifact by path (--pubkey, else PLUGIN_TRUSTED_KEYS).
- config PLUGIN_TRUSTED_KEYS: os.pathsep-separated public-key PEM paths,
  delivered with the site config, never read from the shelf. .env.example
  documents it.
- docs/PLUGIN-SIGNING.md: curator flow (keygen offline, review, pack, publish,
  pin keys, rotate).

The signature proves an artifact is exactly what a curator signed, not that the
code is safe - human review before signing is the control. 11 tests: sign/verify
round trip, wrong key, provenance excludes noise, serialize determinism, pack +
verify, tamper -> hash mismatch, extra file, no-key fail-closed, verify_dir.
1028 pass, naming green.
2026-07-18 20:21:57 -04:00

3.0 KiB

Plugin signing and packaging (curator guide)

ADR-013 Phase 1. How a plugin becomes a signed, verifiable artifact and how a site trusts it. The signature proves an artifact is EXACTLY what a curator reviewed and signed - it does not prove the code is safe. Human review before signing is the actual safety control; the signature makes that review's verdict tamper-evident all the way to the point of execution.

Requires the cryptography package (already a dependency).

One-time: create the publisher key pair

flask plugin keygen --out ./keys --name curator

Writes keys/curator.key (PRIVATE) and keys/curator.pub (public).

  • Keep the .key OFFLINE with the curator. It is the only thing that can sign a trusted artifact. Never put it on the plugin shelf or in the repo.
  • Distribute the .pub with each site's deployed config and pin it (below).
  • Rotation: generate a new pair, pin BOTH public keys on sites for an overlap window (verify accepts any trusted key), then retire the old one.

Per plugin: review, then pack

  1. Review the plugin's source. This is the security gate - read what it does.

  2. Validate and package in one step:

    flask plugin pack printers --key ./keys/curator.key --publisher west-jefferson
    

    pack refuses to sign a directory that does not validate (manifest schema, name/dir match, core_version, dependencies on disk). On success it writes printers-<version>.shopdbplugin - a zip of the plugin plus:

    • PROVENANCE.json: name, version, publisher, created, and a sorted {file: sha256} map of every packaged file.
    • PROVENANCE.sig: a detached ed25519 signature over the exact PROVENANCE.json bytes.
  3. Publish the artifact to the shelf (a SharePoint-synced or copied folder). Transport is untrusted; the signature is what makes it safe.

Verify an artifact

flask plugin validate dist/printers-1.0.0.shopdbplugin --pubkey ./keys/curator.pub

Checks, fail-closed: signature against the trusted key(s), every file's hash, no unexpected files, manifest schema, and that the plugin's core_version admits this framework's contract version. Any changed byte in any file fails the hash check; a signature from an untrusted key fails the signature check.

Pin trusted keys on a site

Set PLUGIN_TRUSTED_KEYS to one or more public-key PEM paths, separated by the OS path separator (: on Linux, ; on Windows), in the site's environment:

PLUGIN_TRUSTED_KEYS=/etc/shopdb/keys/curator.pub:/etc/shopdb/keys/curator-next.pub

Keys are read only from this deployed config, never from the shelf - a folder an attacker could write must not also carry the keys that authenticate it. With no keys set, validate on an artifact fails closed (unverifiable).

What is NOT in this phase

Verification is available but not yet enforced automatically at plugin load or migrate time, and there is no adopt command that pulls from a shelf yet. Those land in Phase 2 (verify-at-load, verify-at-migrate, the signed shelf index). For now, verify artifacts manually with validate before installing.