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.
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
.keyOFFLINE 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
.pubwith each site's deployed config and pin it (below). - Rotation: generate a new pair, pin BOTH public keys on sites for an overlap
window (
verifyaccepts any trusted key), then retire the old one.
Per plugin: review, then pack
-
Review the plugin's source. This is the security gate - read what it does.
-
Validate and package in one step:
flask plugin pack printers --key ./keys/curator.key --publisher west-jeffersonpackrefuses to sign a directory that does not validate (manifest schema, name/dir match, core_version, dependencies on disk). On success it writesprinters-<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 exactPROVENANCE.jsonbytes.
-
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.