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.
This commit is contained in:
75
docs/PLUGIN-SIGNING.md
Normal file
75
docs/PLUGIN-SIGNING.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user