Files
JWeBot/docs/release-artifacts.md
T

183 lines
5.6 KiB
Markdown

# Extension Release Artifacts
Status: digest verification, OpenSSH publisher signing, and local atomic
activation implemented; production host provisioning and activation are not yet
implemented.
## Build
```sh
./tools/package-plugin.py examples/hello-command --output dist
```
The builder:
1. validates `plugin.toml`;
2. applies the static source policy;
3. rejects symlinks and unsupported runtime files;
4. bounds file count and expanded size;
5. hashes canonical relative paths and bytes;
6. creates a deterministic gzip/tar artifact;
7. writes a canonical release record.
Output names are content-addressed:
```text
<plugin-id>-<version>-<source-digest-prefix>.tar.gz
<plugin-id>-<version>-<source-digest-prefix>.release.json
```
## Verify
```sh
./tools/verify-release.py \
dist/<artifact>.tar.gz \
dist/<artifact>.release.json
```
Verification does not extract the archive. It checks:
- artifact name, size, and SHA-256;
- regular-file-only tar members;
- no absolute paths or `..` traversal;
- fixed `plugin/` archive root;
- bounded expanded size and file count;
- exact file list;
- canonical source digest.
## Determinism
File modification times, owners, groups, and host paths are not included in the
artifact. Identical reviewed inputs produce identical artifact bytes and
digests.
If a content-addressed destination already exists with different bytes, the
builder fails instead of replacing it.
## Release Record
The JSON record includes:
- schema version `2`;
- plugin ID, manifest author, and plugin version;
- SDK API version;
- source digest;
- artifact file name;
- artifact SHA-256 and size;
- exact runtime file list.
## Publisher Signing
Signing uses the OpenSSH `sshsig` format with namespace
`jwebot-release-v1`. Its domain-separated payload binds the publisher
principal, release filename, and exact canonical release JSON. The release JSON
already binds the artifact digest, size, source digest, author, and file list.
Create an Ed25519 publisher key outside the repository:
```sh
ssh-keygen -t ed25519 -f ~/.config/jwebot/publisher -C jwebot-publisher
```
Create an OpenSSH allowed-signers file containing only reviewed public keys:
```text
example-publisher ssh-ed25519 AAAA... jwebot-publisher
```
The verifier intentionally accepts only simple entries with explicit
lowercase publisher principals and Ed25519 keys. Wildcards, signer options, and
other key algorithms are rejected.
Build and sign in one step:
```sh
./tools/package-plugin.py examples/hello-command \
--output dist \
--signing-key ~/.config/jwebot/publisher \
--publisher example-publisher
```
An existing canonical release record can be signed separately:
```sh
./tools/sign-release.py dist/<release>.release.json \
--private-key ~/.config/jwebot/publisher \
--publisher example-publisher
```
The signing operation first verifies the referenced artifact's name, digest,
size, archive structure, file list, and source digest. It refuses to sign a
record whose sibling artifact is missing or inconsistent.
The private key must never be committed, copied into an extension artifact, or
installed on production host. The target receives only the reviewed public-key
allowed-signers file.
## Security Boundary
Digest verification proves content integrity relative to the release record.
Publisher verification proves that the exact canonical record was signed by a
key authorized for the declared publisher principal. The manifest `author`
identifies who wrote the plugin; signature `publisher` identifies who approved
and released those exact bytes.
Before production activation, a later phase must still add:
- reviewed public-key provisioning on production host;
- isolated extension-host health checks;
- target Debian `systemd-analyze verify` and socket protocol checks.
production host must never execute a mutable branch checkout or ordinary `git pull`.
## Local Activation Simulation
Activation requires an explicit state root:
```sh
./tools/activate-release.py \
dist/<artifact>.tar.gz \
dist/<artifact>.release.json \
--signature dist/<release>.release.sig.json \
--allowed-signers ~/.config/jwebot/allowed_signers \
--publisher example-publisher \
--state-root /tmp/jwebot-extension-state
```
The activator:
- verifies the artifact before writing release data;
- verifies the publisher signature against the explicit trust file;
- installs under `releases/<full-source-digest>/`;
- preserves the signature record beside the installed release record;
- writes files with private permissions;
- re-verifies an already installed release before reuse;
- starts the isolated Host and requires a matching health response before
switching `current`;
- switches a relative `current` symlink atomically;
- preserves the prior verified target as `previous`;
- serializes changes with a file lock.
Rollback:
```sh
./tools/rollback-release.py \
--state-root /tmp/jwebot-extension-state \
--allowed-signers ~/.config/jwebot/allowed_signers \
--publisher example-publisher
```
Rollback re-verifies the destination content and publisher signature, then
atomically swaps `current` and `previous`. It does not execute plugin code.
Unsigned activation is rejected by default. Tests may opt into the explicit
`--allow-unsigned-local` flag; that flag cannot be combined with signature
arguments and must never be used for production state.
Local packaging tests that cannot launch the installed Host may use
`--skip-health-check-local`. This is also an explicit non-production override;
signed production activation fails closed when no health check is supplied.
These commands have no default production path. A caller must provide
`--state-root`. The repository does not invoke them against production host.