diff --git a/.github/workflows/package-win.yml b/.github/workflows/package-win.yml index 5f659683d..f8c43e1ad 100644 --- a/.github/workflows/package-win.yml +++ b/.github/workflows/package-win.yml @@ -25,6 +25,8 @@ jobs: name: NSIS installer runs-on: windows-latest timeout-minutes: 30 + env: + HAS_CSC: ${{ secrets.CSC_LINK != '' }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -42,6 +44,17 @@ jobs: shell: bash run: rm -rf dist dist-server release - run: pnpm package:win + # Optional Authenticode signing. Runs only when a cert is supplied via + # secrets.CSC_LINK (the same var electron-builder would use). Without it + # the build stays unsigned — the verify step below still rejects a + # publisherName on an unsigned build, so auto-update keeps working. + - name: sign the Windows build (optional) + if: ${{ env.HAS_CSC == 'true' }} + shell: bash + env: + OMB_CERT_FILE: ${{ secrets.CSC_LINK }} + OMB_PFX_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} + run: pwsh scripts/sign-win.ps1 - name: report what was built shell: bash run: | diff --git a/.gitignore b/.gitignore index f915aeedb..e7e601a72 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ release cloudflare/composio-broker/worker-configuration.d.ts .claude/worktrees/ .vercel/ + +# local self-signed test cert (not for production) +build/omb-selfsigned.pfx +>>>>>>> 8f1cea4 (feat(build): add reproducible Windows code-signing step) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..83d2b3e9e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog + +All notable changes to OpenMausBot are documented here. The format is based on +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project +adheres to [Conventional Commits](https://www.conventionalcommits.org/). + +## [Unreleased] + +## [0.1.27] + +### Fixed +- **Windows build: extract Android Platform Tools under git-bash.** `scripts/prepare-android-tools.mjs` + now tries `unzip` and falls back to `tar`, and normalizes absolute Windows paths to + MSYS form (`C:\x` → `/c/x`) so `pnpm build:android-tools` / `pnpm package:win` + succeed on a stock git-bash install instead of failing with + `tar: Cannot connect to C: resolve failed`. ([#317](https://github.com/milind-soni/OpenMausBot/pull/317)) + +### Added +- **Reproducible Windows code-signing step.** `scripts/sign-win.ps1` signs the NSIS + installer and the inner `OpenMausBot.exe` with any PFX — a self-signed cert + (`build/omb-selfsigned.pfx`, gitignored) for pipeline verification, or a real + CA-issued Authenticode cert supplied via `CSC_LINK`/`CSC_KEY_PASSWORD`. + `electron-builder.yml` documents the flow and intentionally keeps `publisherName` + unset so auto-update keeps working on the unsigned build. + Note: a self-signed cert validates only on machines that trust its root; a + CA-issued cert is still required for a SmartScreen-clean install. + +[Unreleased]: https://github.com/milind-soni/OpenMausBot/compare/v0.1.27...HEAD +[0.1.27]: https://github.com/milind-soni/OpenMausBot/releases/tag/v0.1.27 diff --git a/README.md b/README.md index 4a7dfee59..b601010ed 100644 --- a/README.md +++ b/README.md @@ -265,6 +265,25 @@ pnpm package:win # Windows installer + zip → release/ pnpm package:linux # Ubuntu x64 .deb + AppImage → release/ ``` +#### Code-signing the Windows build + +`pnpm package:win` produces an **unsigned** installer (SmartScreen flags it as +"unknown publisher"). Sign it with Authenticode after the build: + +```sh +# self-signed test cert (pipeline verification only — still trips SmartScreen) +pwsh scripts/sign-win.ps1 + +# real CA-issued cert (clean SmartScreen install) +CSC_LINK=path/to/cert.pfx CSC_KEY_PASSWORD=**** pwsh scripts/sign-win.ps1 +``` + +`scripts/sign-win.ps1` signs both `release/OpenMausBot--setup.exe` and the +inner `OpenMausBot.exe` with an RFC-3161 timestamp (DigiCert), so the signature +outlives the cert. A self-signed cert validates only on machines that trust its +root; for a SmartScreen-clean install use a CA-issued Authenticode certificate +and set `win.certificateFile` / `CSC_*` in `electron-builder.yml`. + ### Routines and webhook triggers Routines can run once or on selected weekdays, using either a MAUS's configured model/computer or the diff --git a/electron-builder.yml b/electron-builder.yml index d7935ef24..f6b0a6216 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -108,15 +108,17 @@ win: - target: zip arch: x64 icon: build/icon.ico - # No signing config yet (would go under win.signtoolOptions or - # win.azureSignOptions — eb 26 nests it, there is no top-level - # win.certificateFile). The installer is unsigned, so SmartScreen shows - # "unknown publisher" on first run (README says so). verifyUpdateCodeSignature - # defaults true, but electron-updater skips the check when app-update.yml - # carries no publisherName — so auto-update still works today. Do NOT set - # publisherName without actually signing, or every update is rejected as - # untrusted; and once signed, keep the cert subject stable (or list both old - # and new in publisherName) or you strand already-installed users. + # Signing: the build is unsigned by default (electron-builder needs signtool.exe + # from the Windows SDK, which this repo's CI provides). To produce a SIGNED + # Windows build locally, run scripts/sign-win.ps1 after `pnpm package:win` + # (uses build/omb-selfsigned.pfx for pipeline verification) or supply a real + # Authenticode cert via CSC_LINK/CSC_KEY_PASSWORD + win.certificateFile and + # install signtool. A self-signed cert still trips SmartScreen ("unknown + # publisher") — it only proves the signing pipeline works; for a clean install + # use a CA-issued cert (SSL.com/Sectigo/EV). publisherName stays unset on + # purpose: electron-updater skips signature verification when app-update.yml + # has no publisherName, so auto-update keeps working today. Do NOT set + # publisherName without a REAL cert, or every update is rejected as untrusted. nsis: oneClick: true diff --git a/package.json b/package.json index 29d66525e..80292f749 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "package:prepare": "pnpm build && pnpm build:server && pnpm build:companion && pnpm build:updater && pnpm build:android-tools", "package:mac": "pnpm package:prepare && pnpm build:speech && pnpm build:cua && electron-builder --mac --publish never", "package:win": "pnpm package:prepare && electron-builder --win --publish never", + "package:win:signed": "pnpm package:win && pwsh scripts/sign-win.ps1", "package:linux": "pnpm package:prepare && pnpm build:cua:linux && electron-builder --linux --x64 --publish never", "package:linux:offline": "pnpm package:prepare && pnpm build:cua:linux:offline && electron-builder --linux --x64 --publish never", "smoke:linux-package": "node scripts/run-linux-package-smoke.mjs", diff --git a/scripts/sign-win.ps1 b/scripts/sign-win.ps1 new file mode 100644 index 000000000..15dbc9000 --- /dev/null +++ b/scripts/sign-win.ps1 @@ -0,0 +1,57 @@ +# sign-win.ps1 — sign the built Windows artifacts with Authenticode. +# +# Usage (after `pnpm package:win`): +# pwsh scripts/sign-win.ps1 +# pwsh scripts/sign-win.ps1 -Pfx build/omb-selfsigned.pfx -Password omb-test-2026 +# +# For a REAL cert: pass -Pfx -Password (or set env +# OMB_CERT_FILE / OMB_PFX_PASSWORD). Requires the cert's private key. +# +# NOTE: a self-signed cert only proves the pipeline works. It still shows +# "unknown publisher" on machines that don't trust its root. Add the root to +# Trusted Root CAs (CurrentUser) once to validate locally: +# $pfx = New-Object Security.Cryptography.X509Certificates.X509Certificate2($Pfx, $Password) +# $s = New-Object Security.Cryptography.X509Certificates.X509Store('Root','CurrentUser'); $s.Open('ReadWrite'); $s.Add($pfx); $s.Close() + +param( + [string]$Pfx = $env:OMB_CERT_FILE, + [string]$Password = $env:OMB_PFX_PASSWORD, + [string]$Thumbprint, + [string]$TimestampServer = 'http://timestamp.digicert.com' +) + +$ErrorActionPreference = 'Stop' + +if (-not $Pfx) { $Pfx = Join-Path $PSScriptRoot '..uild\omb-selfsigned.pfx' } +if (-not $Password) { $Password = 'omb-test-2026' } + +$root = Resolve-Path (Join-Path $PSScriptRoot '..') +$ver = (node -p "require('./package.json').version" 2>$null) +if (-not $ver) { throw "Could not read package version from package.json" } + +$installerPath = Join-Path $root "release\OpenMausBot-$ver-setup.exe" +$unpackedAppPath = Join-Path $root 'release\win-unpacked\OpenMausBot.exe' + +$files = @($installerPath, $unpackedAppPath) + +# Throw immediately if any of the required files are missing before signing +foreach ($f in $files) { + if (-not (Test-Path $f)) { + throw "Missing required build artifact for signing: $f" + } +} + +if ($Thumbprint) { + $cert = Get-ChildItem "cert:\CurrentUser\My" | Where-Object { $_.Thumbprint -eq $Thumbprint } + if (-not $cert) { throw "cert with thumbprint $Thumbprint not in CurrentUser\My" } +} else { + $sec = ConvertTo-SecureString -String $Password -Force -AsPlainText + $cert = New-Object Security.Cryptography.X509Certificates.X509Certificate2($Pfx, $sec, 'PersistKeySet') +} + +foreach ($f in $files) { + $r = Set-AuthenticodeSignature -FilePath $f -Certificate $cert -TimestampServer $TimestampServer + Write-Host ("{0,-60} {1}" -f $f, $r.Status) + if ($r.Status -ne 'Valid') { throw ("signing failed: " + $r.StatusMessage) } +} +Write-Host "done."