ADR-013 Phase 2: enforcement + signed shelf + adopt
Completes the marketplace security model. Verification stops being advisory: a plugin only loads or migrates when its tree matches a trusted signature, and plugins are pulled from a signed shelf with anti-rollback and revocation. Enforcement (default OFF - existing deploys unchanged): - verification.py PluginVerifier, shared by the loader (verify-at-load, before plugin.py is imported) and the migration manager (verify-at-migrate, before any DDL). Fail-closed: an unsigned/tampered/wrong-key plugin does not run. - Gated by PLUGIN_REQUIRE_SIGNED. PLUGIN_DEV_TRUST_DIRS exempts named dirs but only under DEBUG/TESTING; production ignores it. - flask plugin stamp-bundled writes provenance into in-tree plugins so verify-at-load applies to bundled plugins too (image build step). - tier:core manifest guard: uninstall/disable refuse a core-tier plugin. Shelf (shelf.py): - Signed shelf-index.json (+ .sig): monotonic serial (a site refuses an older index - anti-rollback), revoked list carried across builds, per-entry version/tier/core_version for browse. Index is a browse layer only; adopt reads security-bearing fields from the verified artifact. - flask plugin shelf-build / shelf-list / adopt / audit. adopt verifies index + artifact (signature + every file hash), unpacks to staging, re-verifies, then atomically moves into place and installs+enables the closure. Refuses a downgrade without --force-downgrade. Anti-rollback serial stored in instance/shelf-state.json. - config PLUGIN_SHELF_DIR; the app only reads the folder, never speaks a network. .env.example + docs/PLUGIN-SIGNING.md document the flow. 22 tests: verifier policy (off / no-keys / signed / tampered / wrong-key / dev-exempt), verify-at-load + verify-at-migrate integration, tier guard, index sign/verify + tamper/wrong-key, serial state, revocation, version resolution, verified atomic unpack + tamper refusal. Live-smoked keygen->pack->shelf-build ->list->adopt->audit + serial guard. 1050 pass, naming green.
This commit is contained in:
@@ -67,9 +67,60 @@ 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
|
||||
## Enforce signatures (Phase 2)
|
||||
|
||||
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.
|
||||
By default nothing is enforced - plugins load unsigned, as before. To require
|
||||
signatures on a site:
|
||||
|
||||
1. Stamp the plugins the image ships with, so verify-at-load applies to them
|
||||
too (run at image build with the site/build key):
|
||||
|
||||
```
|
||||
flask plugin stamp-bundled --key ./keys/curator.key
|
||||
```
|
||||
|
||||
This writes `PROVENANCE.json` + `PROVENANCE.sig` into each in-tree plugin.
|
||||
|
||||
2. Pin the public key(s) and turn enforcement on (site config):
|
||||
|
||||
```
|
||||
PLUGIN_TRUSTED_KEYS=/etc/shopdb/keys/curator.pub
|
||||
PLUGIN_REQUIRE_SIGNED=true
|
||||
```
|
||||
|
||||
Now a plugin only loads (verify-at-load) or migrates (verify-at-migrate) when
|
||||
its tree matches a trusted signature. An unsigned, tampered, or wrong-key
|
||||
plugin is refused - fail-closed. `PLUGIN_DEV_TRUST_DIRS` exempts named
|
||||
directories, but ONLY under DEBUG/TESTING (the external-repo dev workflow);
|
||||
production ignores it.
|
||||
|
||||
## The shelf and adopt (Phase 2)
|
||||
|
||||
A shelf is a read-only folder of artifacts plus a signed index. The app reads
|
||||
`PLUGIN_SHELF_DIR`; it never talks to SharePoint - a sync (or robocopy/USB)
|
||||
populates that folder, and the signature makes the transport untrusted and
|
||||
interchangeable.
|
||||
|
||||
Publish (curator, after packing artifacts into the shelf folder):
|
||||
|
||||
```
|
||||
flask plugin shelf-build --dir /srv/shelf --key ./keys/curator.key --serial 3
|
||||
```
|
||||
|
||||
The index carries a monotonic `serial` (a site refuses an index older than the
|
||||
last it saw) and a `revoked` list (carried forward across builds). Bump
|
||||
`--serial` on every publish.
|
||||
|
||||
On a site:
|
||||
|
||||
```
|
||||
flask plugin shelf-list # browse (verifies index + serial)
|
||||
flask plugin adopt printers # or printers==1.2.0
|
||||
flask plugin audit # warn if an installed version is revoked
|
||||
```
|
||||
|
||||
`adopt` verifies the shelf index and the artifact (signature + every file
|
||||
hash), unpacks into a staging area, re-verifies, then atomically moves it into
|
||||
place and installs + enables it with its dependency closure. It refuses a
|
||||
downgrade unless `--force-downgrade`. Run `flask plugin upgrade-all` and restart
|
||||
afterward so migrations apply and routes register.
|
||||
|
||||
Reference in New Issue
Block a user