Skip to content

fix(install): make provisioning idempotent, and require a checksum (TIGER-77, TIGER-78) - #1

Merged
WebTigers merged 1 commit into
mainfrom
fix/provision-idempotent-and-checksum-required
Sep 10, 2026
Merged

fix(install): make provisioning idempotent, and require a checksum (TIGER-77, TIGER-78)#1
WebTigers merged 1 commit into
mainfrom
fix/provision-idempotent-and-checksum-required

Conversation

@WebTigers

Copy link
Copy Markdown
Owner

Both Astra findings confirmed against real code, then fixed.

TIGER-77 — provisioning destroyed the secrets it had just minted

do_provision() rebuilt local.ini from scratch on every call, writing only the tiger.db.* block. Everything else in the file was destroyed — including tiger.crypto.key and tiger.security.pepper. Tiger_Install::provisionSecrets() then minted replacements, because it generates a secret whenever the key is absent.

Sharper than the ticket states: the routine runs on both the admin and the finish step, so a normal forward pass rewrites the file twice — this was never retry-only. A clean straight-through install survives because nothing is encrypted in between, which is exactly why the authors.host proof passed. But go back and forward once, or retry finish after any error, and the pepper rotates after do_create_owner() hashed the admin password with the old one. The operator is locked out of the site they just installed.

  • local_ini_merge_db() updates only the tiger.db.* keys, preserving every other line and comments
  • write_file_checked() — temp-then-rename, so a failed write cannot leave a half-written config, and failure is reported rather than swallowed
  • A refusal when local.ini already names a different database. do_install_files() returns success early when vendor/autoload.php exists and never reaches its own guard; a genuine resume submits the same database and still passes.

TIGER-78 — the integrity check failed open

Verification was skipped when a release had no .sha256 asset, and skipped again when the fetch came back empty (the $expected guard short-circuited). Precisely the release/download failures a digest exists to catch were the ones that sailed through.

An automatic download now requires a well-formed 64-hex digest that matches, and refuses before extraction otherwise. The refusal happens before the download, so a checksum-less release fails in a second instead of after ~18 MB. Manual upload stays a separate, explicit trust decision.

Verification

  • Every published release carries its .sha256 (checked all 12), and resolve_release() against the live API still returns one — so failing closed breaks no real install.
  • The merge is tested against the real Tiger_Install::provisionSecrets(), loaded from a byte-identical prefix of the shipped installer rather than a copy.
  • Control test included: it reproduces the old wholesale rewrite and shows the pepper rotating — so the bug is proven, not merely asserted.

13 assertions, all green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01L8p9pLJ3DFstG3xZuh2QgZ

…IGER-77, TIGER-78)

TIGER-77 -- do_provision() rebuilt local.ini from scratch on every call, writing
only the tiger.db.* block. Anything already in that file was destroyed, including
tiger.crypto.key and tiger.security.pepper. Tiger_Install::provisionSecrets() then
minted REPLACEMENTS, because it generates a secret whenever the key is absent.

The routine runs on BOTH the admin and the finish step, so this is not a
retry-only edge: a normal forward pass rewrites the file twice. On a clean
straight-through install nothing has been encrypted in between, which is why the
authors.host proof passed. But go back and forward once, or retry finish after any
error, and the pepper rotates AFTER do_create_owner() hashed the admin password
with the previous one -- locking the operator out of the site they just installed.

Fixed by merging instead of replacing: local_ini_merge_db() updates only the
tiger.db.* keys and preserves every other line, comments included. Writes now go
through write_file_checked() (temp-then-rename, so a failure can never leave a
half-written config) and REPORT failure instead of being swallowed by the @
operator. Added a refusal when local.ini already names a DIFFERENT database, since
do_install_files() returns success early when vendor/autoload.php exists and never
reaches its own guard -- a resume submits the same database and still passes.

TIGER-78 -- checksum verification was skipped entirely when a release carried no
.sha256 asset, and skipped again when the fetch returned empty (the $expected
guard short-circuited). Exactly the release/download failures the digest exists to
catch were the ones that went unverified. An automatic download now requires a
well-formed 64-hex digest that matches, and refuses before extraction otherwise.
The refusal happens BEFORE the download, so a checksum-less release fails in a
second rather than after ~18MB. Manual upload is a separate explicit trust
decision and is unchanged.

Verified: every published release carries its .sha256, and resolve_release()
against the live API still returns one, so failing closed breaks no real install.
The merge is proven against the REAL Tiger_Install::provisionSecrets() -- and the
control test reproduces the old destructive rewrite and shows the pepper rotating.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8p9pLJ3DFstG3xZuh2QgZ
@WebTigers
WebTigers merged commit 0bdd88b into main Sep 10, 2026
@WebTigers
WebTigers deleted the fix/provision-idempotent-and-checksum-required branch September 10, 2026 07:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant