PHAR and self-update¶
Run lockrot on any project without adding a dependency to it. Download the archive from the GitHub release, verify it, and point it at the project. It needs PHP 7.4 or newer.
curl -fsSL -O https://github.com/somework/lockrot/releases/latest/download/lockrot.phar
curl -fsSL -O https://github.com/somework/lockrot/releases/latest/download/lockrot.phar.sha256
curl -fsSL -O https://github.com/somework/lockrot/releases/latest/download/lockrot.phar.asc
curl -fsSL https://raw.githubusercontent.com/somework/lockrot/main/lockrot-release-key.asc | gpg --import
sha256sum -c lockrot.phar.sha256 # intact (macOS: shasum -a 256 -c)
gpg --verify lockrot.phar.asc lockrot.phar # compare the printed fingerprint with SECURITY.md
gh attestation verify lockrot.phar --repo somework/lockrot # optional: needs gh, signed in; built by lockrot's release workflow
Run it only when every check above passed and the fingerprint matches:
Update the archive later with php lockrot.phar self-update
(Keeping it updated).
Verifying the download¶
| Check | File | It proves | It needs |
|---|---|---|---|
| sha256 | lockrot.phar.sha256 |
The archive matches the checksum published beside it: the download is intact | sha256sum or shasum |
| GPG signature | lockrot.phar.asc |
The release key signed these bytes, even if the release assets were replaced | gpg and the key fingerprint |
| Build provenance | none (GitHub stores it) | GitHub built these bytes in the PHAR workflow of somework/lockrot |
gh, signed in (gh auth login) |
| Rebuild | none | The bytes come from the tagged source | git, PHP, Composer |
| Self-update signature | lockrot.phar.sig.json |
The self-update key signed these bytes | openssl |
Behind a mirror, still fetch lockrot.phar.sha256 and lockrot.phar.asc from github.com. The
short URL https://lockrot.dev/lockrot.phar redirects to the same GitHub release asset; it is a
convenience, not a source of trust.
To pin a release, replace latest/download with download/<tag> in every URL, for example
https://github.com/somework/lockrot/releases/download/<tag>/lockrot.phar.
Older releases
-
Releases before 0.5.0 publish the checksum only: no
lockrot.phar.ascand no attestation. PHIVE refuses them. -
Releases before 0.6.0 publish no
lockrot.phar.sig.jsonand are not reproducible.
GPG signature¶
The release key is an OpenPGP key whose fingerprint is listed in
SECURITY.md.
Its public half is
lockrot-release-key.asc
in the repository, and it is on keys.openpgp.org and keyserver.ubuntu.com.
-
Import the key, from the repository or from a keyserver by its fingerprint:
-
Verify the archive:
-
Compare the
Primary key fingerprintgpg prints with the fingerprint in SECURITY.md. That comparison is the check: a key file fetched from the same host as the archive proves nothing by itself.
Releases are signed by a subkey that expires and is replaced; the primary key's fingerprint stays the one to trust.
Self-update signature¶
lockrot.phar.sig.json is the signature self-update checks: RSA, PKCS#1 v1.5 over SHA-384, by
the self-update key, in Composer's {"sha384": "<base64>"} format. The archive verifies it with
PHP's openssl extension and the public key built into it, so an update needs no gpg.
To check it by hand, run the lines below. The last one prints the key's fingerprint; compare it with the self-update key in SECURITY.md.
curl -fsSL -O https://github.com/somework/lockrot/releases/latest/download/lockrot.phar.sig.json
curl -fsSL -O https://raw.githubusercontent.com/somework/lockrot/main/lockrot-selfupdate-key.pub
php -r 'echo base64_decode(json_decode(file_get_contents("lockrot.phar.sig.json"), true)["sha384"]);' > lockrot.phar.sig.bin
openssl dgst -sha384 -verify lockrot-selfupdate-key.pub -signature lockrot.phar.sig.bin lockrot.phar
openssl pkey -pubin -in lockrot-selfupdate-key.pub -outform DER | sha256sum # compare with the hex after sha256: in SECURITY.md
Build provenance¶
GitHub attests each release: a signed statement that this exact archive was built by the PHAR
workflow of somework/lockrot from a given commit. Verify it with the
GitHub CLI, signed in with gh auth login:
It needs no lockrot key: the trust root is Sigstore's public-good instance, which GitHub uses for
public repositories. Prefer it where gh is already installed.
Rebuilding it yourself¶
The PHAR is reproducible: the tagged commit and the Composer version the release was built with give the same bytes, on any PHP that the pinned Box runs on. The steps are in CONTRIBUTING.md.
Installing with PHIVE¶
PHIVE downloads the release, verifies its GPG signature and pins the version in
.phive/phars.xml:
PHIVE shows the release key's fingerprint and asks before it trusts the key; compare it with
SECURITY.md. To install without the prompt, pass the fingerprint, without spaces, as
--trust-gpg-keys <fingerprint>.
The Docker image¶
The image ghcr.io/somework/lockrot is published and signed by
lockrot-action, whose README says how to
verify it.
What the PHAR does and does not do¶
-
Commands.
lockrot(aliasrot; the default, so its name can be left out) andself-update(aliasselfupdate), plus Symfony'shelp,listandcompletion. No Composer command, such asinstallorupdate, runs through the PHAR. -
The project directory.
-d <dir>makes<dir>the working directory before lockrot starts. Relative--outputand--baselinepaths are relative to it:php lockrot.phar -d app --output=json:lockrot.jsonwritesapp/lockrot.json. -
No walk-up. The PHAR never moves up to a parent directory's project; Composer's
use-parent-diris not honoured. Run it from the project root or point-dthere. -
Option values. Give lockrot's own options (
--output,--format,--fail-on, …) their value with=(--output=json:r.json): written with a space and no command name before it, the value is read as a command name and the run exits1.-d <dir>takes a space. -
COMPOSER. Honoured as Composer honours it:COMPOSER=alt.json php lockrot.pharreadsalt.jsonandalt.lock(Environment overrides).
What lockrot reads, writes, contacts and never runs, in the PHAR and the plugin alike, is listed in SECURITY.md.
Keeping it updated¶
php lockrot.phar self-update # install the newest release this archive may take
php lockrot.phar self-update --check # report only
self-update downloads the chosen release with its lockrot.phar.sha256 and
lockrot.phar.sig.json. It installs it only when the hash matches, the signature verifies against
the key built into the running archive, and PHP can open the download. A failure at any step leaves
the running file exactly as it was.
| Option | Effect |
|---|---|
| none | Installs the newest release that passes the rules below |
--check |
Decides as a plain run would and installs nothing; exit 1 when that run would install a release. --force does not change what it reports |
--allow-major |
Also takes the next major version, one major version per run |
--force |
Reinstalls the newest release at or below the running version in its major version, when nothing newer is installable |
--offline |
Refuses to run (exit 2), as COMPOSER_DISABLE_NETWORK=1 does: an update needs the network |
-
What
--forcetakes. Only that one release: if the rules below hold it back, or the running major version has none, it exits2without looking further down. A build whose release was withdrawn goes back to the newest release left in its major version, never to an older major version. -
Where it writes. Only the PHAR's own directory, which must be writable; the check comes before the archive is downloaded (
--checkdoes not need it). -
Leftovers. An update killed between the download and the replace can leave a
<name>.<pid>-<id>.tmp.pharfile beside the PHAR. It is inert. The nextself-updatethat downloads a release and verifies it deletes any such file older than an hour. -
No rollback. Every earlier release stays downloadable from GitHub.
-
Rate limit.
LOCKROT_GITHUB_TOKEN,GITHUB_TOKENor Composer'sgithub-oauthlifts GitHub's anonymous rate limit (what is sent where). -
Hosts
--checkreaches.--checkdownloads no archive, but reads release metadata fromgithub.com, which redirects to GitHub's release-asset host (hosts).
Which release it installs¶
self-update reads lockrot's release list from the GitHub API, not through lockrot.dev. It skips
drafts, pre-releases and tags that are not a stable version, and prints a line naming a published
tag, listed above the running version, that is not a version at all (nightly). Versions compare
the way Composer compares them (v1.0, V1.0.0 and 1.0.0 are one version) and print as
major.minor.patch.
It installs the newest release that passes every rule, and prints a line naming the newest release each rule held back:
| Rule | A release that breaks it | What self-update does |
|---|---|---|
| Same major version. The major version is the first number: all of 0.x is one line, and 0.x to 1.0 is a major step | Is in a later major version | Prints it, does not install it |
| A PHP this machine has | Needs a newer PHP than the running one, or gives a malformed PHP floor (not major.minor.patch) |
Skips it |
| A key this archive carries | Is signed with a self-update key the running archive does not carry | Skips it; after a planned key rotation this makes the transition release the step in between (SECURITY.md) |
--allow-major takes the newest release of the next major version that has a stable release, and
no further: from 1.x it installs the newest 2.x even when 3.0 exists. --allow-major is suggested
only when it would install a release.
The PHP floor and the signing key come from the release metadata, lockrot.phar.meta.json, which
each release publishes beside the archive: {"php": "7.4.0", "selfupdate-key": "sha256:…"}.
-
Release metadata that cannot be read, for the release
self-updatewould install, is an error (exit2) naming the tag. -
Release metadata that cannot be read, for a next-major release that is only suggested, gets a line saying so, and the update within the running major version goes ahead.
The release metadata is not signed: it decides which release is tried, and the checksum and the signature decide whether one is installed. What doctored metadata can and cannot do is in SECURITY.md.
Older releases
-
Archives before 0.13.0 install whatever GitHub's latest release is. From them, a new major version installs directly, a release that needs a newer PHP installs and then refuses to start, and a release signed after a key rotation is refused as a signature mismatch: reinstall it by hand. Once such an archive has updated to 0.13.0 or later, the rules above apply.
-
Releases before 0.13.0 publish no
lockrot.phar.meta.json.self-updatereads them as needing PHP 7.4.0 and makes no claim about their key, so the signature alone decides. -
The 0.6.0 archive looks for its signature as
lockrot.phar.sig, which later releases do not publish. It reports the missing asset and stays in place: reinstall it by hand once, or runphive update. -
The 0.5.0 archive checks the checksum only. Releases before 0.6.0 carry no self-update signature, so no archive that checks signatures installs one.
self-update exit codes¶
self-update uses lockrot's exit codes with meanings of its own:
| Code | Meaning |
|---|---|
0 |
A release was installed, or nothing is installable: the build is current in its major version, or every newer release is in a later major version or needs a PHP this machine lacks (rules above), each printed on a line. A newer release held back by its key is exit 2 |
1 |
--check only: self-update would install a release, in the running major version or, with --allow-major, in the next one. A line names it |
2 |
A failure; the running lockrot.phar is untouched |
The exit 2 causes include:
- no published release, or GitHub unreachable;
- the chosen release lacks an asset, or its release metadata cannot be read;
- a checksum mismatch, a signature that does not verify, a PHP whose openssl extension is missing or has no SHA-384, or a download PHP cannot open;
- an archive stranded by a key rotation: a newer release it would take, in the running major
version (or the next one with
--allow-major), is held back only by its key, and no release can be installed (under--checktoo); - an unwritable PHAR directory, or a file that cannot be written or moved beside the PHAR;
--forcewhen the newest release at or below the running version, in its major version, is missing or held back;--offline, orCOMPOSER_DISABLE_NETWORKset to anything but empty or0;- running outside the PHAR (a plugin install updates with
composer update somework/lockrot); - a command line it cannot read (
self-update --nope,--check=yes).
A mistyped command name (php lockrot.phar self-updte) is not a self-update error: it exits 1
as any unknown command does (exit 1 that is not lockrot's). A
unique prefix such as self-up runs self-update.
Reinstalling by hand¶
self-update cannot replace the archive by itself when:
- it is stranded by a key rotation (exit
2, naming the key it does not carry); - the self-update key was compromised: every archive on the old key is replaced this way, whatever its version (SECURITY.md);
- an installed release refuses to start;
- it predates the rules above and the release it would install was signed after a key rotation, or it is the 0.6.0 archive (Older releases under Which release it installs).
To reinstall:
- Download
lockrot.pharagain, as at the top of this page. - Verify it with the GPG signature or the build provenance, not with a key the old archive carried.
- Put it in place of the old file.
Every self-update after that verifies with the key the new download carries.
In CI¶
On GitHub Actions, somework/lockrot-action downloads
the PHAR, checks it and runs it in one step; see ci.md. On any other CI, run
these lines in a shell step, with GITHUB_TOKEN set from the CI's secret store to lift GitHub's
anonymous rate limit:
set -e # stop at the first failed download or check, before php runs
LOCKROT_VERSION=<tag> # the release to run; a pinned tag keeps the archive and its checksum from one release
curl -fsSL -O https://github.com/somework/lockrot/releases/download/$LOCKROT_VERSION/lockrot.phar
curl -fsSL -O https://github.com/somework/lockrot/releases/download/$LOCKROT_VERSION/lockrot.phar.sha256
sha256sum -c lockrot.phar.sha256
gh attestation verify lockrot.phar --repo somework/lockrot # drop this line where the runner lacks gh or GH_TOKEN
php lockrot.phar --fail-on=high --target-php=8.4
Pick the --fail-on value in ci.md.
The global-plugin alternative¶
A global Composer install is the other way to run lockrot outside a project's dependencies, and
composer global update keeps it current:
It is a plugin, not a PHAR, so it prints the install-time summary in every
project you run Composer in. It reads extra.lockrot from each project, not from the global
composer.json; install-time.md lists the switches that turn the summary off.
When it fails¶
| Symptom | Cause | Fix |
|---|---|---|
gpg: Can't check signature: No public key |
The release key is not imported | Import it (GPG signature) and run gpg --verify again |
| gpg warns the key "is not certified with a trusted signature" | No key you trust has vouched for the release key; the signature itself is good | Compare the Primary key fingerprint with SECURITY.md; that comparison is the check |
| gpg reports an expired or unknown subkey | Your copy of the key predates the current signing subkey | Import the key again |
sha256sum: command not found |
macOS ships shasum instead |
shasum -a 256 -c lockrot.phar.sha256 |
sha256sum -c reports FAILED on a fresh download |
A release was published between the two latest downloads, or the archive is not the published one |
Download both again from a pinned tag (download/<tag>); if it still fails, do not run the archive |
self-update exits 2 saying the directory is not writable |
The PHAR sits in a directory you cannot write, such as /usr/local/bin |
Run it with sudo, or reinstall by hand |
self-update --check exits 2 when a newer release exists, on a network that allows only api.github.com |
--check also reads release metadata from github.com (hosts) |
Allow github.com and the host it redirects to, or check on a machine that can reach them |
php lockrot.phar --output json:r.json exits 1 naming a json command |
The value after a space is read as a command name | Write it with =: --output=json:r.json |
self-update exits 2 saying the newer releases are signed with a key this archive does not carry |
The archive is stranded by a key rotation | Reinstall by hand |
Related¶
- SECURITY.md — the key fingerprints, key rotation, and what lockrot reads, writes and contacts
- ci.md — the exit codes of a lockrot run
- configuration.md — every option the PHAR's
lockrotcommand takes - compatibility.md — what stays stable in the PHAR and
self-update - install-time.md — the summary a plugin install prints