Skip to content

fix(scripts): resolve the personal signing key from the repo and overwrite partial signatures - #3904

Merged
dr-bonez merged 5 commits into
masterfrom
fix/manage-release-signingkey-from-repo
Sep 14, 2026
Merged

dr-bonez merged 5 commits into
masterfrom
fix/manage-release-signingkey-from-repo

Conversation

@MattDHill

@MattDHill MattDHill commented Sep 4, 2026

Copy link
Copy Markdown
Member

Summary

  • cmd_sign and cmd_cosign resolve user.signingkey after enter_release_dir has cd'd into the release staging directory, so the repo-local key is never seen and the global one is used. With a global SSH signing key that is a .pub path, and gpg aborts with No secret key after writing the first Start9 signature. This happened cutting start-cli 2.0.0.
  • Read the key from $REPO_ROOT, and keep the personal signature only when the repo's gpg.format is unset or openpgp and gpg --list-secret-keys on PATH knows the key. Otherwise sign skips it with the existing warning and cosign fails before downloading anything. Signing stays on gpg from PATH, the contract the Start9 key, the apt suite signature and pre-check already rely on; Git's gpg.program is deliberately not consulted, since Git guarantees only its -bsau/--verify interface for it and resolves a relative path from the worktree.
  • sign rebuilds signatures/ from empty on every run. cosign extracts the release's current archive into an empty directory and removes the current user's entries before regenerating them, so other maintainers' signatures actually present in the archive survive and nothing left in the staging directory from an earlier run is republished. The start9 signer name is reserved for the organizational signature files.
  • Pass --yes to the detach-sign calls so re-running sign or cosign after a partial failure overwrites the half-written signatures/*.asc instead of stopping at gpg's overwrite prompt.
  • The install guide says additional maintainer signatures may be included in signatures.tar.gz, without asserting who they belong to.

Verified by signing start-cli 2.0.0, start-tunnel 1.3.0 and start-registry 1.1.0 with the patched script.

…write partial signatures

`cmd_sign` and `cmd_cosign` read `user.signingkey` after `enter_release_dir`
has moved into the release staging directory, so a repo-local key is never
seen and the global one is used. With a global SSH signing key that is a
`.pub` path, and gpg aborts with "No secret key" after the first Start9
signature. Read the key from `$REPO_ROOT`, and skip the personal signature
when that repo signs with SSH, which gpg cannot use.

Pass `--yes` to the detach-sign calls so re-running `sign` or `cosign` after a
partial failure overwrites the half-written `signatures/*.asc` instead of
stopping at gpg's overwrite prompt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@MattDHill MattDHill added the repo Repository maintenance label Sep 4, 2026
@MattDHill

Copy link
Copy Markdown
Member Author

@helix-nine review this PR

@helix-nine helix-nine left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed git diff origin/master...HEAD with a high-effort fan-out and independent verification of each candidate. bash -n scripts/manage-release.sh passes.

Two changes are needed:

  1. scripts/manage-release.sh:316-317 — respect the configured OpenPGP format and program. The guard excludes only ssh, but Git also supports x509; an X.509 user.signingkey therefore reaches the literal OpenPGP gpg calls and aborts signing/cosigning with “No secret key.” The new repo-local key lookup can also select a key provided through repo-local gpg.openpgp.program/gpg.program, while the script still invokes literal gpg. Please retain the personal key only when gpg.format is unset or openpgp, and use the configured OpenPGP program (falling back to gpg) for signing and export.

  2. projects/start-os/docs/src/installing-startos.md:127 — align the documented archive contents with SSH signing. This change intentionally omits the releaser signature and key when the repo uses SSH signing, but the user guide unconditionally promises a second releaser signature. Remove that assertion so an expected omission is not mistaken for an incomplete archive.

The comment pass was clean.

…e configured program

`gpg.format` can also be `x509`, whose key the literal `gpg` calls cannot
use, so the personal signature is kept only when the format is unset or
`openpgp`. The sign and export calls for that key go through the repo's
`gpg.openpgp.program`, then `gpg.program`, then `gpg`, so a key that only
a configured program can reach still signs. The install guide no longer
promises the second signature unconditionally.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@MattDHill

Copy link
Copy Markdown
Member Author

Both addressed in 37010e8: the personal key is kept only when gpg.format is unset or openpgp, its sign and export calls go through gpg.openpgp.programgpg.programgpg, and the install guide now says the second signature may be present.

@MattDHill
MattDHill requested a review from helix-nine September 8, 2026 17:53

@helix-nine helix-nine left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the fix as git diff 61ade9ea8b5404b689524bca093d56b030d9f947..37010e8cd3eb849de0668709c36f7045c9101079 with a high-effort fan-out and independent verification. bash -n scripts/manage-release.sh passes. My first review's recommendation to use Git's configured program was underspecified; the fix exposed three additional configuration semantics that need handling.

  1. scripts/manage-release.sh:321-323,1340,1346,1376,1378 — the configured program resolution does not match Git's contract. Three valid configurations fail:

    • gpg.program and gpg.openpgp.program are aliases applied in configuration order, but these separate lookups always prefer any modern-name value. A global gpg.openpgp.program therefore incorrectly beats a repo-local gpg.program.
    • Git runs a slash-relative program from the worktree root; the script invokes it later from the release directory, producing exit 127.
    • Git only guarantees that a custom OpenPGP program supports its -bsau/--verify interface. It need not support the full --detach-sign --armor -o and --export -a interface now assumed here.

    Please preserve Git's alias ordering and relative-path semantics, and do not infer full GnuPG compatibility from the Git setting. The release flow needs either a separately explicit full-GPG contract/override or signing through Git's guaranteed interface with export capability handled separately.

  2. scripts/manage-release.sh:1336-1350 — skipped personal signing can republish stale files. On a sign rerun after the personal signer becomes unavailable or changes to SSH/X.509, the guarded writes are skipped but the previous *.${GH_USER}.asc and ${GH_USER}.key.asc files remain in signatures/ and are archived again. If the staged artifacts changed, those retained signatures are invalid. Ensure skipped regeneration cannot silently republish the current signer's stale files while preserving intentionally retained cosigners.

  3. projects/start-os/docs/src/installing-startos.md:127 — the remaining attribution is still too narrow. cmd_cosign can append signatures from additional maintainers after the release was cut, so there may be multiple signatures and they need not belong to the release cutter. “Additional maintainer signatures may be included” states the archive shape without asserting unsupported provenance.

The comment pass was clean.

…tale copies on rerun

The release tooling's signing contract is GnuPG on PATH: the Start9 key, the
apt suite and pre-check all go through `gpg`, and the personal signature now
does too. Git's `gpg.program` is not consulted: Git guarantees only its own
`-bsau`/`--verify` interface for that program and resolves a relative path
from the worktree, neither of which holds for the `--detach-sign`/`--export`
calls made from the release directory.

A `sign` rerun removes the current signer's earlier `*.<user>.asc` and
`<user>.key.asc` before regenerating, so a signature that is skipped this time
cannot ride along stale. Other maintainers' cosignatures are left in place.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@MattDHill

Copy link
Copy Markdown
Member Author

Addressed in b70b50f.

  1. Rather than reproduce Git's alias ordering and worktree-relative resolution, the script no longer consults gpg.program at all. Its signing contract is GnuPG on PATH, which the Start9 key, the apt suite signature and pre-check already require, and Git guarantees only the -bsau/--verify interface for a configured program anyway. The gpg.format guard stays: the personal key is used only when the format is unset or openpgp.
  2. cmd_sign now deletes the current signer's earlier *.<user>.asc and <user>.key.asc before regenerating, so a skipped signature can't be republished stale; other maintainers' cosignatures are untouched.
  3. Guide now reads "Additional maintainer signatures may be included".

@helix-nine helix-nine left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed git diff 37010e8cd3eb849de0668709c36f7045c9101079..b70b50f044ff741f86406003060bd31ed08ecb65 with a high-effort fan-out and independent verification. The configured-program and documentation findings are addressed, and bash -n scripts/manage-release.sh passes. I also added the required StartOS label for the book change.

Two retry-path issues remain:

  1. scripts/manage-release.sh:316,1338-1344,1359-1376 — a configured key is assumed to be available to PATH gpg. Keeping GnuPG-on-PATH as the release contract is coherent, but a nonempty Git user.signingkey does not prove that this gpg can access it. A stale key or one backed only by Git's custom program passes the guards, then optional personal signing aborts sign after the release has already been tagged/published. cosign downloads the archive before failing the same way. Please preflight the personal key against PATH gpg: skip it with the existing warning in sign, and fail before downloading anything in cosign.

  2. scripts/manage-release.sh:1333-1348,1365-1378 — persistent staging can republish stale signatures. The new cleanup removes only the resolved current user's files. On a forced re-pull, signatures from other maintainers can remain against replaced artifact bytes; if GH_USER resolution fails, no personal files are removed. cosign also extracts the downloaded archive over the persistent local directory, so local-only or obsolete files can be resurrected. Rebuild sign's signature directory from empty. For cosign, extract the current remote archive into an empty directory, then remove the current user's entries before regenerating them; that preserves other signatures actually present in the downloaded archive without retaining local residue.

The comment pass was clean.

…signature dir on every run

A configured `user.signingkey` proves nothing about the `gpg` on PATH, so the
personal key is kept only if `gpg --list-secret-keys` knows it; `sign` then
skips it with the existing warning and `cosign` fails before downloading.

`sign` rebuilds `signatures/` from empty, and `cosign` extracts the release's
current archive into an empty directory and drops the current user's entries
before regenerating them, so nothing left in the staging directory from an
earlier run can ride along against replaced artifacts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@MattDHill

Copy link
Copy Markdown
Member Author

Addressed in a13c38c.

  1. resolve_gh_user now also requires gpg --list-secret-keys "$GH_GPG_KEY" to succeed on PATH gpg; otherwise the key is blanked, so sign skips with the existing warning and cosign hits its guard before downloading.
  2. sign rebuilds signatures/ from empty. cosign extracts the downloaded archive into an empty directory, then removes the current user's *.<user>.asc and <user>.key.asc before regenerating, so only signatures present in the remote archive survive.

@MattDHill
MattDHill requested a review from helix-nine September 8, 2026 19:44
Helix-Harness: pi
Helix-Model: openai-codex/gpt-5.6-sol
@helix-nine

Copy link
Copy Markdown
Contributor

Addressed the remaining signer-namespace collision in effd10284: resolve_gh_user now rejects GH_USER=start9 case-insensitively before either signing path can delete or overwrite the organizational files. Verified with the actual script for start9, Start9, and START9, plus bash -n and git diff --check. I also updated the PR summary to reflect the guard.

@helix-nine helix-nine left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed through effd10284. All requested changes are addressed; the final reserved-namespace guard passes the focused tests, shell syntax check, and diff check.

@dr-bonez
dr-bonez merged commit 02deb85 into master Sep 14, 2026
16 checks passed
@dr-bonez
dr-bonez deleted the fix/manage-release-signingkey-from-repo branch September 14, 2026 17:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

repo Repository maintenance StartOS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants