mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-22 02:56:52 +03:00
Authenticate local release bundles with the installer's pinned keys, require an expected skill version, and provide JSON audit output without extraction or installation. Prepared with AI assistance from Codex under maintainer direction.
155 lines
6.7 KiB
Markdown
155 lines
6.7 KiB
Markdown
# Skill bundle signatures
|
|
|
|
`impeccable install`, `update`, and `check` authenticate a remote skill ZIP
|
|
before extracting it. `universal.zip.sig.json` is an Ed25519 signature over
|
|
the ZIP's SHA-256 digest, byte length, release version, artifact name, and key
|
|
ID. The engine trusts only `scripts/bundle-signing-keys.json`, compiled into
|
|
the binary. A signature cannot introduce a new trusted key.
|
|
|
|
The download endpoint on impeccable.style redirects to a versioned GitHub
|
|
release. The installer resolves that redirect once and downloads the ZIP and
|
|
its signature from that same release. Every subsequent redirect must use
|
|
HTTPS. Missing signatures, unknown keys, changed metadata, and changed ZIP
|
|
bytes stop the operation before extraction or writes to installed skills.
|
|
The temporary download directory is removed on failure.
|
|
|
|
## Verify a downloaded release offline
|
|
|
|
Use an approved engine that includes `verify-bundle`, and obtain the ZIP and
|
|
`universal.zip.sig.json` from the same versioned skill release. Then run:
|
|
|
|
```sh
|
|
impeccable verify-bundle /path/to/universal.zip --version 4.3.1
|
|
```
|
|
|
|
The expected version is required and must be the skill version, not the CLI
|
|
or engine version. The signature defaults to `<zip>.sig.json`; use
|
|
`--signature /path/to/manifest.json` when stored separately. Both
|
|
`--version=4.3.1` and `--version 4.3.1` are accepted.
|
|
|
|
For an audit record:
|
|
|
|
```sh
|
|
impeccable verify-bundle /path/to/universal.zip --version 4.3.1 --json
|
|
```
|
|
|
|
Successful JSON contains `verified: true`, `version`, `artifact`, `keyId`,
|
|
`size`, and `sha256`. Only authenticated metadata is printed. Exit codes are
|
|
0 for successful verification, 1 for verification or file errors, and 2 for
|
|
invalid arguments. Errors go to stderr, including with `--json`; stdout is
|
|
empty on failure.
|
|
|
|
This command reads local files only. It does not download, extract, install,
|
|
or enable hooks, and it ignores local bundle overrides. It uses the same
|
|
compiled-in public keys and verifier as remote installation; there is no
|
|
custom-key or skip-verification option. Approve the engine and its keyring
|
|
through your organization's trust process first. Invoking through `npx` may
|
|
still download the npm package or engine; use an already provisioned native
|
|
binary for a fully offline workflow.
|
|
|
|
Verification authenticates the bytes and their declared release. It does not
|
|
inspect ZIP contents or prove that skill instructions are safe. Requiring an
|
|
expected version rejects a different release, but cannot tell you whether the
|
|
version you chose is the newest. Verify again if the files change before use.
|
|
|
|
## Sign a release
|
|
|
|
Install the 1Password CLI and enable its desktop app integration. The signing
|
|
item holds the PKCS#8 Ed25519 private key in a concealed `private-key` field.
|
|
Set references, not key material, in your shell:
|
|
|
|
```sh
|
|
export OP_ACCOUNT='<account ID or sign-in address>'
|
|
export IMPECCABLE_SIGNING_KEY_REF='op://<vault ID>/<item ID>/private-key'
|
|
bun run release:skill
|
|
```
|
|
|
|
For a persistent setup on your machine, use local Git settings instead:
|
|
|
|
```sh
|
|
git config --local impeccable.signingAccount '<account ID or sign-in address>'
|
|
git config --local impeccable.signingKeyRef 'op://<vault ID>/<item ID>/private-key'
|
|
```
|
|
|
|
Those values stay in `.git/config`, outside version control. Environment
|
|
variables take precedence. Neither setting contains the private key.
|
|
|
|
The release command rebuilds the ZIP, reads the key through `op read`, checks
|
|
that its public key is trusted, and writes the sidecar before creating any
|
|
tag or release. The ZIP and sidecar are uploaded together. The key is never
|
|
passed as a command argument, written to a temporary file, or printed. It
|
|
does exist briefly in the local signing process's memory. 1Password failures
|
|
are reported without forwarding child-process output.
|
|
|
|
`--dry-run` does not access 1Password or create a signature. It checks the
|
|
usual release prerequisites and shows both assets in the upload plan; it
|
|
does not prove that signing credentials work.
|
|
|
|
To sign an already-published release for the initial rollout, download and
|
|
review the exact released `universal.zip`, then run:
|
|
|
|
```sh
|
|
node scripts/sign-bundle.mjs 4.2.0 /path/to/universal.zip
|
|
```
|
|
|
|
Check the resulting sidecar against the verifier and compiled public key:
|
|
|
|
```sh
|
|
impeccable verify-bundle /path/to/universal.zip --version 4.2.0
|
|
```
|
|
|
|
This creates only the local sidecar. It neither uploads it nor replaces the
|
|
ZIP. Never regenerate an old ZIP and sign those different bytes as the old
|
|
release. Uploading the sidecar is a separate maintainer approval step.
|
|
|
|
## Rollout and rotation
|
|
|
|
Before shipping the enforcing engine, publish a valid signature beside the
|
|
exact ZIP currently served by impeccable.style. Verify the pair using a
|
|
locally built engine, then release the engine, its npm platform packages, and
|
|
the CLI/skill pins. Keep the existing release available throughout. Do not
|
|
release an enforcing engine with an empty keyring or an unsigned served ZIP.
|
|
|
|
For planned rotation, ship an engine trusting both the old and new public
|
|
keys before signing with the new key. Older engines that do not know the new
|
|
key will refuse the download and ask for a CLI update. A compromised key
|
|
requires an engine update removing that public key; removing it from a
|
|
website does not revoke trust in already-installed binaries. Keep the
|
|
dedicated signing item separate from GitHub and deployment credentials.
|
|
|
|
## Scope
|
|
|
|
This protects against bundle substitution when an attacker can change the
|
|
download endpoint, release asset, or both, but cannot use the signing key or
|
|
replace the trusted engine. It is not a freshness protocol: a previously
|
|
signed release can still be replayed. Signed timestamp metadata and rollback
|
|
state are separate work. Signatures do not establish that authored skill
|
|
content is safe, and do not authenticate separately downloaded engine
|
|
binaries (those currently use their existing SHA-256 sidecars).
|
|
|
|
`IMPECCABLE_BUNDLE_PATH` and `impeccable link` are explicit local-development
|
|
trust paths. They continue to accept unsigned local files/directories. Do not
|
|
use those overrides to get around a failed remote verification. There is no
|
|
unsigned-network fallback or skip-signature flag.
|
|
|
|
## Wire format
|
|
|
|
JSON sidecar fields: `schema` (1), `keyId`, `version`, `artifact`
|
|
(`universal.zip`), `size`, `sha256`, `signature`. Hex strings are lowercase;
|
|
the public key is 32 bytes and the signature is 64 bytes. Unknown or repeated
|
|
fields are rejected. The signature payload is UTF-8 with LF line endings
|
|
and a final LF:
|
|
|
|
```text
|
|
impeccable-skill-bundle-v1
|
|
<keyId>
|
|
skill-v<version>
|
|
universal.zip
|
|
<size as decimal>
|
|
<sha256 as lowercase hex>
|
|
```
|
|
|
|
The Node signer and Rust verifier share a fixed test vector under
|
|
`tests/fixtures/bundle-signature.json`. Its deterministic test key must never
|
|
be added to the production keyring.
|