feat(FN-5605): add linux gpg signing for release artifacts
Implements Linux GPG signing for the release pipeline by adding a `sign-linux.sh` helper, wiring it into the release and test-release workflows, including `.asc` signature files in release collectors, and documenting the full signing workflow in CODE_SIGNING.md with a note in the desktop README. Fusion-Task-Id: FN-5605 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> Fusion-Task-Id: FN-5605
This commit is contained in:
21
.github/workflows/release.yml
vendored
21
.github/workflows/release.yml
vendored
@@ -252,6 +252,22 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Sign Linux desktop artifacts
|
||||
if: ${{ env.LINUX_GPG_PRIVATE_KEY != '' }}
|
||||
env:
|
||||
LINUX_GPG_PRIVATE_KEY: ${{ secrets.LINUX_GPG_PRIVATE_KEY }}
|
||||
LINUX_GPG_PASSPHRASE: ${{ secrets.LINUX_GPG_PASSPHRASE }}
|
||||
LINUX_GPG_KEY_ID: ${{ secrets.LINUX_GPG_KEY_ID }}
|
||||
shell: bash
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
artifacts=(
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.AppImage
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.deb
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.tar.gz
|
||||
)
|
||||
bash scripts/sign-linux.sh "${artifacts[@]}"
|
||||
|
||||
- name: Generate desktop Linux checksums
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -268,10 +284,13 @@ jobs:
|
||||
path: |
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.AppImage
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.AppImage.sha256
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.AppImage.asc
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.deb
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.deb.sha256
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.deb.asc
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.tar.gz
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.tar.gz.sha256
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.tar.gz.asc
|
||||
|
||||
# ── Create GitHub Release ─────────────────────────────────────────────
|
||||
github-release:
|
||||
@@ -290,7 +309,7 @@ jobs:
|
||||
- name: Collect release files
|
||||
run: |
|
||||
mkdir release-files
|
||||
find artifacts -type f \( -name "fn-*" -o -name "*.sha256" -o -name "*.exe" -o -name "*.exe.sha256" -o -name "*.blockmap" -o -name "*.dmg" -o -name "*.dmg.sha256" -o -name "*.zip" -o -name "*.zip.sha256" -o -name "*.AppImage" -o -name "*.AppImage.sha256" -o -name "*.deb" -o -name "*.deb.sha256" -o -name "*.tar.gz" -o -name "*.tar.gz.sha256" \) -exec cp {} release-files/ \;
|
||||
find artifacts -type f \( -name "fn-*" -o -name "*.sha256" -o -name "*.asc" -o -name "*.exe" -o -name "*.exe.sha256" -o -name "*.blockmap" -o -name "*.dmg" -o -name "*.dmg.sha256" -o -name "*.zip" -o -name "*.zip.sha256" -o -name "*.AppImage" -o -name "*.AppImage.sha256" -o -name "*.deb" -o -name "*.deb.sha256" -o -name "*.tar.gz" -o -name "*.tar.gz.sha256" \) -exec cp {} release-files/ \;
|
||||
ls -la release-files/
|
||||
|
||||
- name: Create GitHub Release
|
||||
|
||||
21
.github/workflows/test-release.yml
vendored
21
.github/workflows/test-release.yml
vendored
@@ -255,6 +255,22 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Sign Linux desktop artifacts
|
||||
if: ${{ env.LINUX_GPG_PRIVATE_KEY != '' }}
|
||||
env:
|
||||
LINUX_GPG_PRIVATE_KEY: ${{ secrets.LINUX_GPG_PRIVATE_KEY }}
|
||||
LINUX_GPG_PASSPHRASE: ${{ secrets.LINUX_GPG_PASSPHRASE }}
|
||||
LINUX_GPG_KEY_ID: ${{ secrets.LINUX_GPG_KEY_ID }}
|
||||
shell: bash
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
artifacts=(
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.AppImage
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.deb
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.tar.gz
|
||||
)
|
||||
bash scripts/sign-linux.sh "${artifacts[@]}"
|
||||
|
||||
- name: Generate desktop Linux checksums
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -271,10 +287,13 @@ jobs:
|
||||
path: |
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.AppImage
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.AppImage.sha256
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.AppImage.asc
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.deb
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.deb.sha256
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.deb.asc
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.tar.gz
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.tar.gz.sha256
|
||||
packages/desktop/dist-electron/Fusion-*-linux-*.tar.gz.asc
|
||||
|
||||
# ── Collect all artifacts ─────────────────────────────────────────────
|
||||
collect:
|
||||
@@ -291,7 +310,7 @@ jobs:
|
||||
- name: Combine artifacts
|
||||
run: |
|
||||
mkdir combined
|
||||
find artifacts -type f \( -name "fn-*" -o -name "*.sha256" -o -name "*.exe" -o -name "*.exe.sha256" -o -name "*.blockmap" -o -name "*.dmg" -o -name "*.dmg.sha256" -o -name "*.zip" -o -name "*.zip.sha256" -o -name "*.AppImage" -o -name "*.AppImage.sha256" -o -name "*.deb" -o -name "*.deb.sha256" -o -name "*.tar.gz" -o -name "*.tar.gz.sha256" \) -exec cp {} combined/ \;
|
||||
find artifacts -type f \( -name "fn-*" -o -name "*.sha256" -o -name "*.asc" -o -name "*.exe" -o -name "*.exe.sha256" -o -name "*.blockmap" -o -name "*.dmg" -o -name "*.dmg.sha256" -o -name "*.zip" -o -name "*.zip.sha256" -o -name "*.AppImage" -o -name "*.AppImage.sha256" -o -name "*.deb" -o -name "*.deb.sha256" -o -name "*.tar.gz" -o -name "*.tar.gz.sha256" \) -exec cp {} combined/ \;
|
||||
ls -la combined/
|
||||
|
||||
- name: Upload combined archive
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Code Signing Setup Guide
|
||||
|
||||
This document explains how to configure code signing for `kb` release binaries so they don't trigger OS security warnings on macOS (Gatekeeper) or Windows (SmartScreen).
|
||||
This document explains how to configure code signing for `kb` release binaries so they don't trigger OS security warnings on macOS (Gatekeeper) or Windows (SmartScreen), and how Linux desktop artifact signatures are published for authenticity verification.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -8,7 +8,7 @@ The release workflow automatically signs binaries when the appropriate secrets a
|
||||
|
||||
- **macOS**: Codesign with hardened runtime + Apple notarization
|
||||
- **Windows**: Authenticode signing with timestamp
|
||||
- **Linux**: No signing (no standard code signing requirement for Linux CLI tools)
|
||||
- **Linux**: GPG detached-signature signing of `.AppImage` / `.deb` / `.tar.gz` desktop artifacts
|
||||
|
||||
Signing is **optional** — if secrets are not configured, the build succeeds and signing steps are skipped.
|
||||
|
||||
@@ -32,6 +32,14 @@ Signing is **optional** — if secrets are not configured, the build succeeds an
|
||||
| `WINDOWS_CERTIFICATE_BASE64` | Base64-encoded `.pfx` Authenticode code signing certificate |
|
||||
| `WINDOWS_CERTIFICATE_PASSWORD` | Password for the `.pfx` certificate |
|
||||
|
||||
### Linux Signing
|
||||
|
||||
| Secret | Description |
|
||||
|--------|-------------|
|
||||
| `LINUX_GPG_PRIVATE_KEY` | Base64 of an ASCII-armored exported private key (`gpg --armor --export-secret-keys`) |
|
||||
| `LINUX_GPG_PASSPHRASE` | Passphrase that unlocks the exported private key |
|
||||
| `LINUX_GPG_KEY_ID` | Long key ID or full fingerprint used with `gpg --local-user` |
|
||||
|
||||
## macOS Setup Instructions
|
||||
|
||||
### 1. Obtain a Developer ID Application Certificate
|
||||
@@ -71,6 +79,53 @@ You can find it by running:
|
||||
security find-identity -v -p codesigning
|
||||
```
|
||||
|
||||
## Linux Signing
|
||||
|
||||
### 1. Generate a release signing key
|
||||
|
||||
```bash
|
||||
gpg --full-generate-key
|
||||
```
|
||||
|
||||
Recommended: RSA 4096, with either no expiration or a 2-year expiration.
|
||||
|
||||
### 2. Export and base64-encode the private key for GitHub Actions secret storage
|
||||
|
||||
```bash
|
||||
gpg --armor --export-secret-keys <key-id> | base64 -w0 | pbcopy
|
||||
```
|
||||
|
||||
Linux clipboard alternative:
|
||||
|
||||
```bash
|
||||
gpg --armor --export-secret-keys <key-id> | base64 -w0 | xclip -selection clipboard
|
||||
```
|
||||
|
||||
Store this output as `LINUX_GPG_PRIVATE_KEY`.
|
||||
|
||||
### 3. Set Linux signing secrets
|
||||
|
||||
Set repository-level Actions secrets:
|
||||
- `LINUX_GPG_PRIVATE_KEY`
|
||||
- `LINUX_GPG_PASSPHRASE`
|
||||
- `LINUX_GPG_KEY_ID`
|
||||
|
||||
### 4. Publish the public key for users
|
||||
|
||||
Export and publish the public key in release notes and/or a repository `KEYS` file:
|
||||
|
||||
```bash
|
||||
gpg --armor --export <key-id>
|
||||
```
|
||||
|
||||
### 5. User verification flow
|
||||
|
||||
```bash
|
||||
gpg --import KEYS && gpg --verify Fusion-<version>-linux-x64.AppImage.asc Fusion-<version>-linux-x64.AppImage
|
||||
```
|
||||
|
||||
If `LINUX_GPG_PRIVATE_KEY` is absent (forks/initial setup), CI continues and ships unsigned Linux artifacts without `.asc` sidecars.
|
||||
|
||||
## Windows Setup Instructions
|
||||
|
||||
### 1. Obtain an Authenticode Code Signing Certificate
|
||||
@@ -104,8 +159,9 @@ Paste the result as the `WINDOWS_CERTIFICATE_BASE64` secret.
|
||||
2. Each platform job builds the standalone binary
|
||||
3. **macOS jobs**: `scripts/sign-macos.sh` runs codesign + notarization
|
||||
4. **Windows jobs**: `scripts/sign-windows.ps1` runs Authenticode signing
|
||||
5. Checksums are generated **after** signing (so they match the signed binaries)
|
||||
6. Signed binaries and checksums are uploaded to the GitHub Release
|
||||
5. **Linux desktop jobs**: `scripts/sign-linux.sh` runs `gpg --detach-sign --armor` for each `.AppImage` / `.deb` / `.tar.gz` artifact and verifies each signature with `gpg --verify`
|
||||
6. Checksums are generated **after** signing (so they match the signed binaries)
|
||||
7. Signed binaries and checksums are uploaded to the GitHub Release
|
||||
|
||||
The test-release workflow (`workflow_dispatch`) includes the same signing steps but guards them with secret-availability checks — signing is skipped if secrets are not configured.
|
||||
|
||||
@@ -139,6 +195,21 @@ Desktop Windows packaging (`.github/workflows/desktop-windows.yml`) now uses the
|
||||
|
||||
- Double-check the `WINDOWS_CERTIFICATE_PASSWORD` secret matches the password used when exporting the `.pfx`
|
||||
|
||||
### Linux signing skipped
|
||||
|
||||
- Expected when `LINUX_GPG_PRIVATE_KEY` is not set (forks, dry runs, first-time setup)
|
||||
- In this path, Linux artifacts upload without `.asc` sidecars by design
|
||||
|
||||
### Linux signing fails with "No secret key"
|
||||
|
||||
- `LINUX_GPG_KEY_ID` does not match the imported private key fingerprint/key ID
|
||||
- Re-export and re-check key ID via `gpg --list-secret-keys --keyid-format=long`
|
||||
|
||||
### Linux verification fails with "BAD signature"
|
||||
|
||||
- Artifact changed between sign and verify; investigate runner storage path and artifact staging
|
||||
- Ensure signatures are generated from the exact file that is later uploaded
|
||||
|
||||
### Signing step skipped
|
||||
|
||||
- In the test-release workflow, signing is intentionally skipped when secrets are not configured
|
||||
|
||||
@@ -357,8 +357,20 @@ Desktop packaging is configured in `electron-builder.yml`.
|
||||
- macOS: `Fusion-<version>-mac-arm64.dmg`, `Fusion-<version>-mac-x64.dmg`, matching `.zip` variants, `.sha256` sidecars, and `.blockmap` files.
|
||||
- Linux: `Fusion-<version>-linux-x64.AppImage` with `.sha256`, plus best-effort `.deb` and `.tar.gz` outputs (and sidecars) when available on the runner image.
|
||||
- Tag-less release rehearsal workflow (`.github/workflows/test-release.yml`) mirrors that artifact collection path without publishing a real GitHub Release.
|
||||
- macOS Developer ID signing/notarization and Linux signing are not wired in these release jobs yet; release-attached macOS/Linux artifacts are currently unsigned and follow-up tasks will add signing flows.
|
||||
- Linux desktop artifacts now include detached GPG signature sidecars: `*.AppImage.asc`, `*.deb.asc`, and `*.tar.gz.asc` when Linux signing secrets are configured in CI.
|
||||
- Linux `.deb` and `.tar.gz` outputs are best-effort and may be absent on some runner images without failing the release.
|
||||
|
||||
### Linux signing
|
||||
|
||||
Every signed Linux desktop release includes `*.asc` detached signature sidecars alongside the binary artifacts.
|
||||
|
||||
Verification example:
|
||||
|
||||
```bash
|
||||
gpg --import KEYS && gpg --verify Fusion-<version>-linux-x64.AppImage.asc Fusion-<version>-linux-x64.AppImage
|
||||
```
|
||||
|
||||
The public key (`KEYS`) must be distributed out-of-band. See `docs/CODE_SIGNING.md` for canonical setup, key publication, and troubleshooting guidance.
|
||||
- Isolated manual Windows build path: `.github/workflows/desktop-windows.yml` (`workflow_dispatch` on `windows-latest`) runs `electron-builder --win --x64 --arm64 --publish never`.
|
||||
- ARM64 artifacts are cross-built on the `windows-latest` x64 runner; execution/validation still requires a Windows ARM64 device or emulator.
|
||||
- Deep link protocol: `fusion://`
|
||||
|
||||
@@ -59,3 +59,44 @@ describe("desktop release workflow wiring", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("desktop linux signing wiring", () => {
|
||||
it("wires Linux GPG secret-guarded signing and asc uploads in both workflows", async () => {
|
||||
const release = await readRepoFile(".github/workflows/release.yml");
|
||||
const testRelease = await readRepoFile(".github/workflows/test-release.yml");
|
||||
|
||||
for (const workflow of [release, testRelease]) {
|
||||
expect(workflow).toContain("secrets.LINUX_GPG_PRIVATE_KEY");
|
||||
expect(workflow).toContain("secrets.LINUX_GPG_PASSPHRASE");
|
||||
expect(workflow).toContain("secrets.LINUX_GPG_KEY_ID");
|
||||
expect(workflow).toContain("LINUX_GPG_PRIVATE_KEY != ''");
|
||||
expect(workflow).toContain("scripts/sign-linux.sh");
|
||||
expect(workflow).toContain("Fusion-*-linux-*.AppImage.asc");
|
||||
expect(workflow).toContain("Fusion-*-linux-*.deb.asc");
|
||||
expect(workflow).toContain("Fusion-*-linux-*.tar.gz.asc");
|
||||
expect(workflow).not.toMatch(/echo\s+["']?\$\{?\s*(secrets\.)?LINUX_GPG_PASSPHRASE/);
|
||||
expect(workflow).not.toMatch(/cat\s+["']?\$\{?\s*(secrets\.)?LINUX_GPG_PASSPHRASE/);
|
||||
}
|
||||
});
|
||||
|
||||
it("collectors in both workflows include asc signatures", async () => {
|
||||
const release = await readRepoFile(".github/workflows/release.yml");
|
||||
const testRelease = await readRepoFile(".github/workflows/test-release.yml");
|
||||
|
||||
expect(release).toContain('-name "*.asc"');
|
||||
expect(testRelease).toContain('-name "*.asc"');
|
||||
});
|
||||
|
||||
it("sign-linux.sh contains expected guarded gpg sign and verify shape", async () => {
|
||||
const script = await readRepoFile("scripts/sign-linux.sh");
|
||||
|
||||
expect(script).toContain("set -euo pipefail");
|
||||
expect(script).toContain("LINUX_GPG_PRIVATE_KEY");
|
||||
expect(script).toContain("LINUX_GPG_PASSPHRASE");
|
||||
expect(script).toContain("LINUX_GPG_KEY_ID");
|
||||
expect(script).toContain("gpg --verify");
|
||||
expect(script).toContain("--detach-sign");
|
||||
expect(script).toContain("--armor");
|
||||
expect(script).toContain("Linux signing skipped (LINUX_GPG_PRIVATE_KEY not set)");
|
||||
});
|
||||
});
|
||||
|
||||
72
scripts/sign-linux.sh
Executable file
72
scripts/sign-linux.sh
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
# sign-linux.sh — Create GPG detached ASCII signatures for Linux release artifacts.
|
||||
#
|
||||
# Usage:
|
||||
# LINUX_GPG_PRIVATE_KEY=... LINUX_GPG_PASSPHRASE=... LINUX_GPG_KEY_ID=... \
|
||||
# bash scripts/sign-linux.sh path/to/file [path/to/file...]
|
||||
#
|
||||
# Environment variables:
|
||||
# LINUX_GPG_PRIVATE_KEY — Base64-encoded ASCII-armored private key
|
||||
# LINUX_GPG_PASSPHRASE — Passphrase for private key
|
||||
# LINUX_GPG_KEY_ID — Key ID or fingerprint used for --local-user
|
||||
#
|
||||
# Exit semantics:
|
||||
# 0 when signing is skipped (missing LINUX_GPG_PRIVATE_KEY),
|
||||
# 0 on successful sign + verify for all files,
|
||||
# non-zero on signing/verification/import failures.
|
||||
#
|
||||
# Do not invoke outside CI unless GNUPGHOME is isolated.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "$#" -eq 0 ]]; then
|
||||
echo "ERROR: No artifact paths provided."
|
||||
echo "Usage: $0 <artifact-path> [artifact-path ...]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${LINUX_GPG_PRIVATE_KEY:-}" ]]; then
|
||||
echo "Linux signing skipped (LINUX_GPG_PRIVATE_KEY not set)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -z "${LINUX_GPG_PASSPHRASE:-}" ]]; then
|
||||
echo "ERROR: LINUX_GPG_PASSPHRASE is required when signing is enabled."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${LINUX_GPG_KEY_ID:-}" ]]; then
|
||||
echo "ERROR: LINUX_GPG_KEY_ID is required when signing is enabled."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
GNUPGHOME="${RUNNER_TEMP:-$(mktemp -d)}/gnupg-fusion"
|
||||
mkdir -p "$GNUPGHOME"
|
||||
chmod 700 "$GNUPGHOME"
|
||||
export GNUPGHOME
|
||||
|
||||
cleanup() {
|
||||
unset LINUX_GPG_PASSPHRASE
|
||||
rm -rf "$GNUPGHOME"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "$LINUX_GPG_PRIVATE_KEY" | base64 -d | gpg --batch --import
|
||||
|
||||
for file in "$@"; do
|
||||
if [[ ! -f "$file" ]]; then
|
||||
echo "ERROR: Artifact not found: $file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gpg --batch --yes \
|
||||
--pinentry-mode loopback \
|
||||
--passphrase "$LINUX_GPG_PASSPHRASE" \
|
||||
--local-user "$LINUX_GPG_KEY_ID" \
|
||||
--armor \
|
||||
--detach-sign \
|
||||
--output "${file}.asc" \
|
||||
"$file"
|
||||
|
||||
gpg --verify "${file}.asc" "$file"
|
||||
done
|
||||
Reference in New Issue
Block a user