Skip to content

[3262 by Claude Opus 5.5] Rotate Linux release signing identity after owner key replacement - #3263

Merged
integry merged 1 commit into
mainfrom
3262/claude-opus-5-5-rotate-linux-release-sign-20261019-1412-f46
Oct 19, 2026
Merged

integry merged 1 commit into
mainfrom
3262/claude-opus-5-5-rotate-linux-release-sign-20261019-1412-f46

Conversation

@propr-dev

@propr-dev propr-dev Bot commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

AI Implementation Summary

Closes #3262

Branch: 3262/claude-opus-5-5-rotate-linux-release-sign-20261019-1412-f46
Commits: ✅ Changes committed (9eec9e1)


AI Processing Completed

Execution Details:

Summary:
I've rotated the Linux release signing pin to the new owner key (80FE0A7667A92A0AD3CEAE6D4C2FF8C3CC4E1599). The focused signing, release and workflow tests pass with real GnuPG and RPM tools, none skipped.

Checking the key: before using the public key from the issue, I inspected it myself with GnuPG 2.2.40. It is a single RSA4096 primary key that can sign, with no subkeys. The user ID is exactly ProPR Desktop Release Signing, it was issued 2026-10-19 and expires 2028-10-18. Its self-signature is valid, and it contains no private key material.

Changes

  • Pin: apps/desktop/scripts/rpm-signing.mjs now pins the new fingerprint. Nothing else in the signing code changed: the existing checks and the 13-asset allowlist are as before, with no broader signer list and no fallback.
  • Public keys: docs/static/keys/propr-desktop-release.asc is now the exact export from the issue. The old export is kept byte-for-byte as propr-desktop-release-historical-DD4E7F65CE2345A1F4597D643CF856B53093D3A3.asc.
  • linux-release-key.md:
    • New key: the new identity, its effective date and download, and what it signs (RPMs, SHA256SUMS and the APT metadata). DEB files are unchanged and get no embedded-signature claim.
    • Rotation and old key: the rotation is not cross-signed by the old key and is backed only by the reviewed docs and the repository. The notice says no compromise was reported and the old key is not revoked, and gives its fingerprint and dates.
    • Older releases: they keep their original signing and trust classification, and the new pin does not authenticate them.
    • Consumer steps: for RPM, import the new key and expect key ID cc4e1599. For APT, replace the keyring the repository line points to.
  • linux-rpm-signing.md: the same identity change. It also explains that signing fails closed until an administrator sets the replacement secrets and public settings to match the pin. The fingerprint in the acceptance example is updated.

Tests

  • I updated the test fixture fingerprint in linux-preview-release.test.mjs and rewrote the owner-key test in rpm-signing.test.mjs. It checks the new key's identity and dates and the old key's identity and dates. It also checks that each key file is rejected under the other's fingerprint.
  • A new test covers the production environment pin. It rejects the old fingerprint, a throwaway wrong signer, a lowercase or short fingerprint, and a missing one. It accepts only the new fingerprint with the new key file, and rejects the new fingerprint paired with the old key or a wrong key.
  • The required command (rpm-signing, linux-preview-release and linux-preview-workflow tests with PROPR_REQUIRE_RPM_TESTS=1): 29 of 29 pass, 0 skipped.
  • The other related acceptance and smoke tests: 44 pass. One existing test skipped because it needs root, and it is unrelated to signing.
  • ESLint is clean, and the docs build succeeds with both key files served under /keys/.

The container had no GnuPG or RPM tools, so I unpacked the Debian bookworm packages into /tmp/tools without installing anything system-wide.

I did not touch GitHub secrets or variables, permissions, deployments, publishing, PR3256 or the existing release validation. No private keys were handled; the tests use throwaway keys only. These are documentation and signing-script changes with nothing visual, so I made no preview.

Before the next signing run, an administrator still needs to replace the protected private key and passphrase secrets. They must also set PROPR_DESKTOP_RPM_FINGERPRINT and PROPR_DESKTOP_RPM_PUBLIC_KEY to the new fingerprint and key file. Until then, signing will refuse to run.

Detailed Logs:

  • Conversation: 101 messages
  • Session: 68b304be-2018-4207-b79a-4b8b845145ba

Log files stored at:

  • conversation: /usr/src/app/logs/executions/issue-3262-2026-10-19T14-19-55-762Z-conversation.json
  • output: /usr/src/app/logs/executions/issue-3262-2026-10-19T14-19-55-762Z-output.txt
Latest Conversation Messages
ASSISTANT: [content unavailable]

ASSISTANT: I've rotated the Linux release signing pin to the new owner key (`80FE0A7667A92A0AD3CEAE6D4C2FF8C3CC4E1599`). The focused signing, release and workflow tests pass with real GnuPG and RPM tools, none s...


This PR was created automatically by ProPR after processing issue #3262.


💡 Need changes?

Comment on this PR to request refinements — the AI agent monitors comments and will update the implementation based on your feedback. Keep iterating until you're satisfied!

…E0A7667A92A0AD3CEAE6D4C2FF8C3CC4E1599`). The focused signing, release and workflow tests pass with real GnuPG and RPM tools, none skipped.

Checking the key: before using the public key from the issue, I inspected it myself with GnuPG 2.2.40. It is a single RSA4096 primary key that can sign, with no subkeys. The user ID is exactly `ProPR Desktop Release Signing`, it was issued 2026-10-19 and expires 2028-10-18. Its self-signature is valid, and it contains no private key material.

Changes
- Pin: `apps/desktop/scripts/rpm-signing.mjs` now pins the new fingerprint. Nothing else in the signing code changed: the existing checks and the 13-asset allowlist are as before, with no broader signer list and no fallback.
- Public keys: `docs/static/keys/propr-desktop-release.asc` is now the exact export from the issue. The old export is kept byte-for-byte as `propr-desktop-release-historical-DD4E7F65CE2345A1F4597D643CF856B53093D3A3.asc`.
- `linux-release-key.md`:
  - New key: the new identity, its effective date and download, and what it signs (RPMs, `SHA256SUMS` and the APT metadata). DEB files are unchanged and get no embedded-signature claim.
  - Rotation and old key: the rotation is not cross-signed by the old key and is backed only by the reviewed docs and the repository. The notice says no compromise was reported and the old key is not revoked, and gives its fingerprint and dates.
  - Older releases: they keep their original signing and trust classification, and the new pin does not authenticate them.
  - Consumer steps: for RPM, import the new key and expect key ID `cc4e1599`. For APT, replace the keyring the repository line points to.
- `linux-rpm-signing.md`: the same identity change. It also explains that signing fails closed until an administrator sets the replacement secrets and public settings to match the pin. The fingerprint in the acceptance example is updated.

Tests
- I updated the test fixture fingerprint in `linux-preview-release.test.mjs` and rewrote the owner-key test in `rpm-signing.test.mjs`. It checks the new key's identity and dates and the old key's identity and dates. It also checks that each key file is rejected under the other's fingerprint.
- A new test covers the production environment pin. It rejects the old fingerprint, a throwaway wrong signer, a lowercase or short fingerprint, and a missing one. It accepts only the new fingerprint with the new key file, and rejects the new fingerprint paired with the old key or a wrong key.
- The required command (`rpm-signing`, `linux-preview-release` and `linux-preview-workflow` tests with `PROPR_REQUIRE_RPM_TESTS=1`): 29 of 29 pass, 0 skipped.
- The other related acceptance and smoke tests: 44 pass. One existing test skipped because it needs root, and it is unrelated to signing.
- ESLint is clean, and the docs build succeeds with both key files served under `/keys/`.

The container had no GnuPG or RPM tools, so I unpacked the Debian bookworm packages into `/tmp/tools` without installing anything system-wide.

I did not touch GitHub secrets or variables, permissions, deployments, publishing, PR3256 or the existing release validation. No private keys were handled; the tests use throwaway keys only. These are documentation and signing-script changes with nothing visual, so I made no preview.

Before the next signing run, an administrator still needs to replace the protected private key and passphrase secrets. They must also set `PROPR_DESKTOP_RPM_FINGERPRINT` and `PROPR_DESKTOP_RPM_PUBLIC_KEY` to the new fingerprint and key file. Until then, signing will refuse to run.
@integry

integry commented Oct 19, 2026

Copy link
Copy Markdown
Owner

/review gpt-6-astra

@propr-dev

propr-dev Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ AI Code Review Complete requested by @integry

Posted 1 review:

View Task Details

@propr-dev

propr-dev Bot commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

🔍 AI Code Review — gpt-6-astra

Overall Evaluation

The PR rotates the Linux preview signing identity with a narrowly scoped production change, preserves the historical public export, and documents the consumer and administrator transition. It is merge-ready within the supplied scope, conditional on pending checks completing successfully.

✅ Exact production pin — rpm-signing.mjs changes only the owner fingerprint and its explanatory comment. The production environment still requires an exact match, with no previous-key fallback.

✅ Preserved authentication boundaries — The supplied callers continue enforcing primary-key validation, both RPM signatures, signed checksums, both APT signatures, preservation checks, and the 13-asset allowlist. Protected workflow configuration is unchanged.

✅ Rotation-specific regression coverage — Added tests exercise rejection of the previous fingerprint, malformed fingerprints, unrelated signers, and mismatched public exports. The historical export matches the previous export shown in the diff.

This review used static analysis only; no commands or tests were run. The issue specification is explicitly truncated, so compliance with its unseen remainder—including comparison against its original public-key attachment—could not be established. The authoritative check summary reports no failures and 15 pending checks.

Merge blockers

No merge blockers.

Suggestions

These are optional follow-ups and are not sent to /fix.

S1: 🟢 Assert historical export byte preservation

The new historical-export test checks identity, dates, and key structure, but does not enforce the documented byte-for-byte preservation claim. An assertion against a fixed digest of the previous export would protect that archival property against future reserialization. This is optional because the historical export in the current diff already matches the removed public export.

Score

The visible implementation satisfies the rotation objective without introducing a verified regression, and adds focused negative tests. Pending checks and the stated static-review limitations qualify the assessment.

Score: 9/10


🤖 Review Details

  • Model: GPT-6 Astra
  • Time: 24s
  • Tokens: 59,456 (58,992 in / 464 out)
  • Cost: $0.48

View Task


💡 Next step: Comment /fix to address every F# merge blocker, /fix all for every pending blocker and suggestion, or name records explicitly, as in /fix F3 S5.
F# and S# IDs increment across review comments and remain permanent, so selectors such as /fix F3 F5 stay unambiguous across cycles. S# suggestions stay optional: they are implemented only when you name them or request /fix all, and they never relax a merge blocker.

Copy and edit to choose records from this review:

/fix S1

🤖 Review by ProPR

@integry
integry merged commit ebf85ed into main Oct 19, 2026
58 checks passed
propr-dev Bot pushed a commit that referenced this pull request Oct 19, 2026
…ted Preview workflow. The full related suite passes with real GnuPG 2.2.40 and RPM 4.18: 53 of 54 pass and none fail. The one skip is the existing root-only acceptance-evidence export test, which is unrelated to signing and was also skipped in #3263. The verify-key check itself can't succeed in tests, because that needs the real stored secret. The tests prove the logic with disposable keys and prove that the real pinned key fails against a disposable private key.

Workflow (`.github/workflows/desktop-linux-signed-preview.yml`)
- `verify-key` is a new option on the existing `operation` input. The identity job adds one branch for it, which requires `unsigned_run_id` and `signing_run_id` to be empty. `source_revision` still goes through the existing 40-hex and main-reachable checks, and is used only as metadata in the receipt.
- The new `verify-key` job runs only for a main-dispatched `workflow_dispatch` with that operation, after the identity and `verify-helpers` jobs.
- It reuses the `desktop-linux-rpm-signing` environment, the `PROPR_DESKTOP_RPM_SIGNING_AUTHORIZED=1` check and the existing secret and variable names. Its only permission is `contents: read`.
- It checks out only `github.sha` and confirms `git rev-parse HEAD` matches it. It installs only `gnupg`.
- It never downloads anything or checks out the frozen source, and it runs no package, release or publish step.
- The two secrets appear in exactly one step's environment, and no action receives them.
- `sign-draft`, `publish-draft` and their gates are unchanged.
- On success the receipt goes to the run summary and to the artifact `propr-desktop-linux-signing-key-verification-<run_id>`.

Script (`apps/desktop/scripts/signing-key-verification.mjs`)
- Secrets: it reads both secrets and deletes them from its environment before any subprocess starts. The environment fingerprint must equal `OWNER_FINGERPRINT` exactly.
- Signing: it reuses the existing `signMetadata` and `verifyMetadata` (built on `withKey` and the two-minute bounded `run`).
- Steps: in a fresh 0700 home it applies the existing RSA, expiry and revocation checks to the public key. Next it imports the private key and requires exactly one secret primary key with the full fingerprint. It then makes a SHA256 detached signature of a fixed challenge text, passing the passphrase through a 0600 file.
- Independent verification: the signature is verified twice, each in a new public-only home: once with the public key from the environment variable, and once with the reviewed export `docs/static/keys/propr-desktop-release.asc`.
- Cleanup: all temporary homes sit under one owned directory. Before reporting success, it confirms that directory is empty and that no GnuPG agent it started is still running. It never inspects or signals any other process.
- Output: a short fixed error message on failure, and no receipt. On success the receipt contains the run ID, the control SHA, the full fingerprint, digests of the challenge and signature, and the passed checks.

Tests
- `signing-key-verification.test.mjs` (new, disposable keys only):
  - a correct pair succeeds.
  - each of these fails: wrong passphrase, wrong or multi-key private key, public-key or pinned-export mismatch, expired key, revoked key, and malformed credentials.
  - tampered, missing, empty, truncated, SHA1, wrong-key and altered-challenge signatures are rejected.
  - leftover homes and agents that won't stop make cleanup fail.
  - the receipt rejects bad run IDs, SHAs and fingerprints.
- The CLI tests:
  - the strict production pin rejects other, lowercase, short and missing fingerprints.
  - the real pinned public key with a disposable private key is a mismatch.
  - a recording fake `gpg` shows that no secret reaches a subprocess's environment or arguments, and no receipt is written on any failure.
- `linux-preview-workflow.test.mjs` now checks the new job's trust boundary and that nothing in it builds, signs or publishes. The existing secrets-count assertion is now checked per job (2 in `sign`, 2 in `verify-key`, 0 elsewhere); none were relaxed.
- The new test runs in the `verify-helpers` gate (before the credentialed job) and in PR validation (`desktop-release-guard.yml`).

Docs: `docs/docs/operations/linux-rpm-signing.md` has a new section with the operator dispatch command (`gh workflow run desktop-linux-signed-preview.yml --ref main -f operation=verify-key -f source_revision=…`), what to approve, how to download and check the receipt, and what fails. The required test command now includes the new test file.

The container had no GnuPG or RPM tools, so I unpacked the Debian packages into `/tmp/tools` without installing anything system-wide. I did not run `verify-key`, touch real secrets or settings, or change any permissions or environment rules. This change has nothing visual, so I made no preview.
integry pushed a commit that referenced this pull request Oct 19, 2026
…out creating release assets (#3265)

* I've added the `verify-key` operation to the Desktop Linux Authenticated Preview workflow. The full related suite passes with real GnuPG 2.2.40 and RPM 4.18: 53 of 54 pass and none fail. The one skip is the existing root-only acceptance-evidence export test, which is unrelated to signing and was also skipped in #3263. The verify-key check itself can't succeed in tests, because that needs the real stored secret. The tests prove the logic with disposable keys and prove that the real pinned key fails against a disposable private key.

Workflow (`.github/workflows/desktop-linux-signed-preview.yml`)
- `verify-key` is a new option on the existing `operation` input. The identity job adds one branch for it, which requires `unsigned_run_id` and `signing_run_id` to be empty. `source_revision` still goes through the existing 40-hex and main-reachable checks, and is used only as metadata in the receipt.
- The new `verify-key` job runs only for a main-dispatched `workflow_dispatch` with that operation, after the identity and `verify-helpers` jobs.
- It reuses the `desktop-linux-rpm-signing` environment, the `PROPR_DESKTOP_RPM_SIGNING_AUTHORIZED=1` check and the existing secret and variable names. Its only permission is `contents: read`.
- It checks out only `github.sha` and confirms `git rev-parse HEAD` matches it. It installs only `gnupg`.
- It never downloads anything or checks out the frozen source, and it runs no package, release or publish step.
- The two secrets appear in exactly one step's environment, and no action receives them.
- `sign-draft`, `publish-draft` and their gates are unchanged.
- On success the receipt goes to the run summary and to the artifact `propr-desktop-linux-signing-key-verification-<run_id>`.

Script (`apps/desktop/scripts/signing-key-verification.mjs`)
- Secrets: it reads both secrets and deletes them from its environment before any subprocess starts. The environment fingerprint must equal `OWNER_FINGERPRINT` exactly.
- Signing: it reuses the existing `signMetadata` and `verifyMetadata` (built on `withKey` and the two-minute bounded `run`).
- Steps: in a fresh 0700 home it applies the existing RSA, expiry and revocation checks to the public key. Next it imports the private key and requires exactly one secret primary key with the full fingerprint. It then makes a SHA256 detached signature of a fixed challenge text, passing the passphrase through a 0600 file.
- Independent verification: the signature is verified twice, each in a new public-only home: once with the public key from the environment variable, and once with the reviewed export `docs/static/keys/propr-desktop-release.asc`.
- Cleanup: all temporary homes sit under one owned directory. Before reporting success, it confirms that directory is empty and that no GnuPG agent it started is still running. It never inspects or signals any other process.
- Output: a short fixed error message on failure, and no receipt. On success the receipt contains the run ID, the control SHA, the full fingerprint, digests of the challenge and signature, and the passed checks.

Tests
- `signing-key-verification.test.mjs` (new, disposable keys only):
  - a correct pair succeeds.
  - each of these fails: wrong passphrase, wrong or multi-key private key, public-key or pinned-export mismatch, expired key, revoked key, and malformed credentials.
  - tampered, missing, empty, truncated, SHA1, wrong-key and altered-challenge signatures are rejected.
  - leftover homes and agents that won't stop make cleanup fail.
  - the receipt rejects bad run IDs, SHAs and fingerprints.
- The CLI tests:
  - the strict production pin rejects other, lowercase, short and missing fingerprints.
  - the real pinned public key with a disposable private key is a mismatch.
  - a recording fake `gpg` shows that no secret reaches a subprocess's environment or arguments, and no receipt is written on any failure.
- `linux-preview-workflow.test.mjs` now checks the new job's trust boundary and that nothing in it builds, signs or publishes. The existing secrets-count assertion is now checked per job (2 in `sign`, 2 in `verify-key`, 0 elsewhere); none were relaxed.
- The new test runs in the `verify-helpers` gate (before the credentialed job) and in PR validation (`desktop-release-guard.yml`).

Docs: `docs/docs/operations/linux-rpm-signing.md` has a new section with the operator dispatch command (`gh workflow run desktop-linux-signed-preview.yml --ref main -f operation=verify-key -f source_revision=…`), what to approve, how to download and check the receipt, and what fails. The required test command now includes the new test file.

The container had no GnuPG or RPM tools, so I unpacked the Debian packages into `/tmp/tools` without installing anything system-wide. I did not run `verify-key`, touch real secrets or settings, or change any permissions or environment rules. This change has nothing visual, so I made no preview.

* feat(ai): I fixed the failure. Five of the new signing-key verification tests failed on the macOS runner, and the fix is to run them only on Linux, which is where the `verify-key` job runs. The workflow and the verification script are unchanged.

I fixed the failure. Five of the new signing-key verification tests failed on the macOS runner, and the fix is to run them only on Linux, which is where the `verify-key` job runs. The workflow and the verification script are unchanged.

**Why they failed.** The log shows `# fail 5` in "Typecheck and test unsigned desktop runtime", all from `apps/desktop/scripts/signing-key-verification.test.mjs`:
- **Four tests** could not create their disposable keys: `gpg-agent` failed to start (`can't connect to the gpg-agent: IPC connect call failed`). macOS temp paths under `/var/folders/...` are too long for GnuPG's agent socket.
- **The cleanup test** failed because the cleanup check reads `/proc`, which macOS doesn't have. The check correctly refuses to report success when it can't confirm cleanup.

The macOS runner has `gpg` installed, so these tests ran there instead of skipping as they do on runners without it. The `verify-key` job itself only runs on `ubuntu-24.04`.

**The change** is in `apps/desktop/scripts/signing-key-verification.test.mjs`. On anything other than Linux, the five tests that need GnuPG or `/proc` now skip with the reason "Requires the Linux verify-key runner". On Linux nothing changes: they still run, and `PROPR_REQUIRE_RPM_TESTS=1` still fails the run if `gpg` or `gpgconf` is missing. The receipt-binding test doesn't need either, so it still runs everywhere.

**Testing:**
- **Linux, real GnuPG 2.2.40:** all 6 tests pass, none skipped. The container has no `gpg`, so I extracted the Debian packages and ran them through small wrappers that point at the extracted agent.
- **Simulated macOS** (platform forced to `darwin`): 1 passes, 5 skip, none fail.
- **Not yet confirmed:** this hasn't been run on a real macOS runner. CI on the next commit will confirm it.

PR: #3265
Comment by: @propr-dev[bot] (ID: 6270557043)
Model: Claude Opus 5.5

---------

Co-authored-by: propr-dev[bot] <1316198+propr-dev[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rotate Linux release signing identity after owner key replacement

1 participant