8000 ci: wire SignPath Authenticode signing behind secrets, pin publisher, document updater kill switch (fixes #71) by Rajveerx11 · Pull Request #74 · amirlehmam/wmux · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
8000
Diff view
139 changes: 76 additions & 63 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ jobs:
build-sign-release:
runs-on: windows-latest

# SignPath Authenticode signing (issue #71). The signing steps run only when
# both secrets are configured, so releases keep working while SignPath OSS
# approval is pending — and turn on automatically once the secrets land.
env:
SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}
SIGNPATH_ORGANIZATION_ID: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}

steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -55,70 +62,9 @@ jobs:
echo "tag=v$VER" >> "$GITHUB_OUTPUT"
fi

# ── Sign exe with SignPath (disabled — waiting for OSS approval) ──
# Uncomment the 3 steps below once SignPath OSS quota is available:
#
# - name: Submit exe to SignPath for Authenticode signing
# id: sign-submit
# shell: bash
# run: |
# HTTP_RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}" \
# -X POST \
# -H "Authorization: Bearer ${{ secrets.SIGNPATH_API_TOKEN }}" \
# -F "projectSlug=wmux" \
10000 # -F "signingPolicySlug=wmux" \
# -F "artifactConfigurationSlug=initial" \
# -F "description=CI release v${{ steps.version.outputs.version }}" \
# -F "artifact=@release/win-unpacked/wmux.exe" \
# -D headers.txt \
# "https://app.signpath.io/api/v1/${{ secrets.SIGNPATH_ORGANIZATION_ID }}/SigningRequests/SubmitWithArtifact")
# HTTP_CODE=$(echo "$HTTP_RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2)
# echo "HTTP status: $HTTP_CODE"
# if [ "$HTTP_CODE" != "201" ]; then
# echo "::error::SignPath submission failed (HTTP $HTTP_CODE)"
# echo "$HTTP_RESPONSE"
# exit 1
# fi
# LOCATION=$(grep -i "^location:" headers.txt | tr -d '\r')
# SIGNING_REQUEST_ID=$(echo "$LOCATION" | grep -oE '[0-9a-f-]{36}$')
# echo "Signing request ID: $SIGNING_REQUEST_ID"
# echo "signing_request_id=$SIGNING_REQUEST_ID" >> "$GITHUB_OUTPUT"
#
# - name: Wait for signing to complete
# id: sign-wait
# shell: bash
# run: |
# ORG="${{ secrets.SIGNPATH_ORGANIZATION_ID }}"
# TOKEN="${{ secrets.SIGNPATH_API_TOKEN }}"
# REQ_ID="${{ steps.sign-submit.outputs.signing_request_id }}"
# API="https://app.signpath.io/api/v1/$ORG/SigningRequests/$REQ_ID"
# for i in $(seq 1 60); do
# RESPONSE=$(curl -s -H "Authorization: Bearer $TOKEN" "$API")
# STATUS=$(echo "$RESPONSE" | jq -r '.status')
# echo "[$i/60] Status: $STATUS"
# if [ "$STATUS" = "Completed" ]; then
# echo "Signing completed successfully!"
# exit 0
# elif [ "$STATUS" = "Failed" ] || [ "$STATUS" = "Denied" ] || [ "$STATUS" = "Canceled" ]; then
# echo "::error::Signing request $STATUS"
# echo "$RESPONSE" | jq .
# exit 1
# fi
# sleep 10
# done
# echo "::error::Signing timed out after 10 minutes"
# exit 1
#
# - name: Download signed exe
# shell: bash
# run: |
# curl -s \
# -H "Authorization: Bearer ${{ secrets.SIGNPATH_API_TOKEN }}" \
# -o "release/win-unpacked/wmux.exe" \
# "https://app.signpath.io/api/v1/${{ secrets.SIGNPATH_ORGANIZATION_ID }}/SigningRequests/${{ steps.sign-submit.outputs.signing_request_id }}/SignedArtifact"
# echo "Signed exe downloaded, size: $(wc -c < release/win-unpacked/wmux.exe) bytes"

# ── Embed icon + metadata into exe (rcedit) ───────────
# NOTE: rcedit rewrites the PE and would invalidate an Authenticode
# signature, so it must run BEFORE the SignPath steps below.
- name: Embed icon and version metadata
shell: bash
run: |
Expand All @@ -142,6 +88,73 @@ jobs:
env:
VER: ${{ steps.version.outputs.version }}

# ── Sign exe with SignPath (runs only when secrets are set, #71) ──
- name: Submit exe to SignPath for Authenticode signing
id: sign-submit
if: env.SIGNPATH_API_TOKEN != '' && env.SIGNPATH_ORGANIZATION_ID != ''
shell: bash
run: |
HTTP_RESPONSE=$(curl -s -w "\nHTTP_CODE:%{http_code}" \
-X POST \
-H "Authorization: Bearer $SIGNPATH_API_TOKEN" \
-F "projectSlug=wmux" \
-F "signingPolicySlug=wmux" \
-F "artifactConfigurationSlug=initial" \
-F "description=CI release v${{ steps.version.outputs.version }}" \
-F "artifact=@release/win-unpacked/wmux.exe" \
-D headers.txt \
"https://app.signpath.io/api/v1/$SIGNPATH_ORGANIZATION_ID/SigningRequests/SubmitWithArtifact")
HTTP_CODE=$(echo "$HTTP_RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2)
echo "HTTP status: $HTTP_CODE"
if [ "$HTTP_CODE" != "201" ]; then
echo "::error::SignPath submission failed (HTTP $HTTP_CODE)"
echo "$HTTP_RESPONSE"
exit 1
fi
LOCATION=$(grep -i "^location:" headers.txt | tr -d '\r')
SIGNING_REQUEST_ID=$(echo "$LOCATION" | grep -oE '[0-9a-f-]{36}$')
echo "Signing request ID: $SIGNING_REQUEST_ID"
echo "signing_request_id=$SIGNING_REQUEST_ID" >> "$GITHUB_OUTPUT"

- name: Wait for signing to complete
if: steps.sign-submit.outcome == 'success' && steps.sign-submit.outputs.signing_request_id != ''
shell: bash
run: |
REQ_ID="${{ steps.sign-submit.outputs.signing_request_id }}"
API="https://app.signpath.io/api/v1/$SIGNPATH_ORGANIZATION_ID/SigningRequests/$REQ_ID"
for i in $(seq 1 60); do
RESPONSE=$(curl -s -H "Authorization: Bearer $SIGNPATH_API_TOKEN" "$API")
STATUS=$(echo "$RESPONSE" | jq -r '.status')
echo "[$i/60] Status: $STATUS"
if [ "$STATUS" = "Completed" ]; then
echo "Signing completed successfully!"
exit 0
elif [ "$STATUS" = "Failed" ] || [ "$STATUS" = "Denied" ] || [ "$STATUS" = "Canceled" ]; then
echo "::error::Signing request $STATUS"
echo "$RESPONSE" | jq .
exit 1
fi
sleep 10
done
echo "::error::Signing timed out after 10 minutes"
exit 1

- name: Download signed exe and verify signature
if: steps.sign-submit.outcome == 'success' && steps.sign-submit.outputs.signing_request_id != ''
shell: bash
run: |
curl -s \
-H "Authorization: Bearer $SIGNPATH_API_TOKEN" \
-o "release/win-unpacked/wmux.exe" \
"https://app.signpath.io/api/v1/$SIGNPATH_ORGANIZATION_ID/SigningRequests/${{ steps.sign-submit.outputs.signing_request_id }}/SignedArtifact"
echo "Signed exe downloaded, size: $(wc -c < release/win-unpacked/wmux.exe) bytes"
# Fail the release if the artifact we are about to ship is not validly signed.
powershell -NoProfile -Command "
\$sig = Get-AuthenticodeSignature 'release/win-unpacked/wmux.exe';
Write-Host ('Status: ' + \$sig.Status + ' — Subject: ' + \$sig.SignerCertificate.Subject);
if (\$sig.Status -ne 'Valid') { exit 1 }
"

# ── Create release zip ─────────────────────────────────
- name: Create release zip
shell: pwsh
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,22 @@ Download [wmux-0.7.10-win-x64.zip](https://github.com/amirlehmam/wmux/releases/l

> **Note:** After extracting, right-click the zip before extracting and select **Unblock** if Windows SmartScreen warns about the executable.

### Updates & security

wmux checks GitHub Releases for updates. Downloaded updates are held in a
quarantine window (3 days by default) before installing, and installs always
require an explicit confirmation click — nothing is applied silently.

Release artifacts are **not yet Authenticode-signed** (SignPath OSS approval is
pending; the CI signing pipeline is wired and activates automatically once the
signing secrets are configured). Until signing lands, security-sensitive or
air-gapped environments can control the updater with environment variables:

| Variable | Effect |
|----------|--------|
| `WMUX_DISABLE_UPDATER=1` | Disable the auto-updater entirely (update manually from GitHub Releases) |
| `WMUX_MIN_RELEASE_AGE_DAYS=N` | Change the quarantine window (default 3 days) |

### From source

```bash
Expand Down
1 change: 1 addition & 0 deletions electron-builder.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
],
"win": {
"icon": "resources/icons/icon.ico",
"publisherName": ["SignPath Foundation"],
"target": [
{ "target": "dir", "arch": ["x64"] }
]
Expand Down
8 changes: 6 additions & 2 deletions src/main/updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ import { fetchLatestRelease } from './update-checker';
// 2. No silent install — autoDownload/autoInstallOnAppQuit are off; the user
// must explicitly confirm the install via a dialog.
//
// (Signature verification + Authenticode + build provenance are tracked as
// follow-ups in the issue; they need offline keys / CI changes, not just code.)
// Authenticode signing is wired in CI (issue #71): release.yml signs wmux.exe
// via SignPath once the SIGNPATH_* secrets are configured, and electron-builder
// pins publisherName ("SignPath Foundation") so signed exe/NSIS update flows
// verify the publisher. The current zip-based update artifact cannot itself be
// Authenticode-verified by electron-updater, so the quarantine window below
// remains the primary client-side control until the artifact format changes.

const DEFAULT_MIN_RELEASE_AGE_DAYS = 3;
const RECHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
Expand Down
0