5.6 KiB
Extension Release Artifacts
Status: digest verification, OpenSSH publisher signing, and local atomic activation implemented; production host provisioning and activation are not yet implemented.
Build
./tools/package-plugin.py examples/hello-command --output dist
The builder:
- validates
plugin.toml; - applies the static source policy;
- rejects symlinks and unsupported runtime files;
- bounds file count and expanded size;
- hashes canonical relative paths and bytes;
- creates a deterministic gzip/tar artifact;
- writes a canonical release record.
Output names are content-addressed:
<plugin-id>-<version>-<source-digest-prefix>.tar.gz
<plugin-id>-<version>-<source-digest-prefix>.release.json
Verify
./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:
ssh-keygen -t ed25519 -f ~/.config/jwebot/publisher -C jwebot-publisher
Create an OpenSSH allowed-signers file containing only reviewed public keys:
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:
./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:
./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 verifyand 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:
./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
currentsymlink atomically; - preserves the prior verified target as
previous; - serializes changes with a file lock.
Rollback:
./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.