diff --git a/.github/workflows/nightly-release.yml b/.github/workflows/nightly-release.yml index f7426a46..43a7f7cd 100644 --- a/.github/workflows/nightly-release.yml +++ b/.github/workflows/nightly-release.yml @@ -45,9 +45,13 @@ jobs: - os: windows-latest config: buildScripts/electron-builder-win.json artifact_name: windows-x64-build + win_arch: x64 + win_unpacked_dir: win-unpacked - os: windows-latest config: buildScripts/electron-builder-win-arm64.json artifact_name: windows-arm64-build + win_arch: arm64 + win_unpacked_dir: win-arm64-unpacked - os: macos-latest config: buildScripts/electron-builder-mac.json artifact_name: macos-build @@ -107,9 +111,9 @@ jobs: SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} - - name: Package application (Windows) + - name: Build application directory (Windows) if: matrix.os == 'windows-latest' - run: node ./buildScripts/package.js --config=${{ matrix.config }} + run: pnpm exec electron-builder --config ${{ matrix.config }} --dir --${{ matrix.win_arch }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SUPABASE_URL: ${{ secrets.SUPABASE_URL }} @@ -120,7 +124,7 @@ jobs: SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} - - name: Sign Windows artifacts with Azure Trusted Signing + - name: Sign application binaries with Azure Trusted Signing (Windows) if: matrix.os == 'windows-latest' uses: azure/artifact-signing-action@v1 with: @@ -130,47 +134,44 @@ jobs: endpoint: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }} signing-account-name: ${{ secrets.TRUSTED_SIGNING_ACCOUNT_NAME }} certificate-profile-name: ${{ secrets.TRUSTED_SIGNING_CERTIFICATE_PROFILE }} - files-folder: ${{ github.workspace }}/build - files-folder-filter: exe,msi + files-folder: ${{ github.workspace }}/build/${{ matrix.win_unpacked_dir }} + files-folder-filter: exe,dll files-folder-recurse: true timestamp-rfc3161: http://timestamp.acs.microsoft.com timestamp-digest: SHA256 description: Power Platform ToolBox (Insider) description-url: https://github.com/PowerPlatformToolBox/desktop-app - - name: Repackage portable ZIP with signed EXE (Windows) + - name: Package installers from signed directory (Windows) if: matrix.os == 'windows-latest' - shell: powershell - run: | - $buildDir = "${{ github.workspace }}/build" - $zipFiles = @(Get-ChildItem "$buildDir/*.zip" -ErrorAction SilentlyContinue) - - if ($zipFiles.Count -eq 0) { - Write-Host "No ZIP artifacts found; skipping repack." - exit 0 - } - - foreach ($zip in $zipFiles) { - Write-Host "Repacking ZIP: $($zip.Name)" - $tempDir = Join-Path $env:RUNNER_TEMP ([Guid]::NewGuid().ToString()) - New-Item -ItemType Directory -Path $tempDir | Out-Null - - Expand-Archive -Path $zip.FullName -DestinationPath $tempDir -Force - - $zipExeFiles = @(Get-ChildItem $tempDir -Recurse -Filter *.exe -ErrorAction SilentlyContinue) - foreach ($zipExe in $zipExeFiles) { - $signedExe = Get-ChildItem $buildDir -Recurse -Filter $zipExe.Name -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($signedExe) { - Copy-Item $signedExe.FullName $zipExe.FullName -Force - Write-Host " Replaced $($zipExe.Name) with signed binary." - } else { - Write-Host " No signed match found for $($zipExe.Name)." - } - } + run: pnpm exec electron-builder --config ${{ matrix.config }} --prepackaged build/${{ matrix.win_unpacked_dir }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SUPABASE_URL: ${{ secrets.SUPABASE_URL }} + SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} + APPINSIGHTS_CONNECTION_STRING: ${{ secrets.APPINSIGHTS_CONNECTION_STRING }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} - Remove-Item $zip.FullName -Force - Compress-Archive -Path (Join-Path $tempDir '*') -DestinationPath $zip.FullName -Force - } + - name: Sign Windows installers with Azure Trusted Signing + if: matrix.os == 'windows-latest' + uses: azure/artifact-signing-action@v1 + with: + azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} + azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} + azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} + endpoint: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }} + signing-account-name: ${{ secrets.TRUSTED_SIGNING_ACCOUNT_NAME }} + certificate-profile-name: ${{ secrets.TRUSTED_SIGNING_CERTIFICATE_PROFILE }} + files-folder: ${{ github.workspace }}/build + files-folder-filter: exe,msi + files-folder-recurse: false + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + description: Power Platform ToolBox (Insider) + description-url: https://github.com/PowerPlatformToolBox/desktop-app - name: Regenerate latest.yml with correct SHA256 hashes (Windows) if: matrix.os == 'windows-latest' diff --git a/.github/workflows/prod-release.yml b/.github/workflows/prod-release.yml index 3113e1b8..59e4f621 100644 --- a/.github/workflows/prod-release.yml +++ b/.github/workflows/prod-release.yml @@ -76,9 +76,13 @@ jobs: - os: windows-latest config: buildScripts/electron-builder-win.json artifact_name: windows-x64-release + win_arch: x64 + win_unpacked_dir: win-unpacked - os: windows-latest config: buildScripts/electron-builder-win-arm64.json artifact_name: windows-arm64-release + win_arch: arm64 + win_unpacked_dir: win-arm64-unpacked - os: macos-latest config: buildScripts/electron-builder-mac.json artifact_name: macos-release @@ -133,9 +137,9 @@ jobs: SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} - - name: Package application (Windows) + - name: Build application directory (Windows) if: matrix.os == 'windows-latest' - run: node ./buildScripts/package.js --config=${{ matrix.config }} + run: pnpm exec electron-builder --config ${{ matrix.config }} --dir --${{ matrix.win_arch }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SUPABASE_URL: ${{ secrets.SUPABASE_URL }} @@ -146,7 +150,7 @@ jobs: SENTRY_ORG: ${{ secrets.SENTRY_ORG }} SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} - - name: Sign Windows artifacts with Azure Trusted Signing + - name: Sign application binaries with Azure Trusted Signing (Windows) if: matrix.os == 'windows-latest' uses: azure/artifact-signing-action@v1 with: @@ -156,47 +160,44 @@ jobs: endpoint: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }} signing-account-name: ${{ secrets.TRUSTED_SIGNING_ACCOUNT_NAME }} certificate-profile-name: ${{ secrets.TRUSTED_SIGNING_CERTIFICATE_PROFILE }} - files-folder: ${{ github.workspace }}/build - files-folder-filter: exe,msi + files-folder: ${{ github.workspace }}/build/${{ matrix.win_unpacked_dir }} + files-folder-filter: exe,dll files-folder-recurse: true timestamp-rfc3161: http://timestamp.acs.microsoft.com timestamp-digest: SHA256 description: Power Platform ToolBox description-url: https://github.com/PowerPlatformToolBox/desktop-app - - name: Repackage portable ZIP with signed EXE (Windows) + - name: Package installers from signed directory (Windows) if: matrix.os == 'windows-latest' - shell: powershell - run: | - $buildDir = "${{ github.workspace }}/build" - $zipFiles = @(Get-ChildItem "$buildDir/*.zip" -ErrorAction SilentlyContinue) - - if ($zipFiles.Count -eq 0) { - Write-Host "No ZIP artifacts found; skipping repack." - exit 0 - } - - foreach ($zip in $zipFiles) { - Write-Host "Repacking ZIP: $($zip.Name)" - $tempDir = Join-Path $env:RUNNER_TEMP ([Guid]::NewGuid().ToString()) - New-Item -ItemType Directory -Path $tempDir | Out-Null - - Expand-Archive -Path $zip.FullName -DestinationPath $tempDir -Force - - $zipExeFiles = @(Get-ChildItem $tempDir -Recurse -Filter *.exe -ErrorAction SilentlyContinue) - foreach ($zipExe in $zipExeFiles) { - $signedExe = Get-ChildItem $buildDir -Recurse -Filter $zipExe.Name -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($signedExe) { - Copy-Item $signedExe.FullName $zipExe.FullName -Force - Write-Host " Replaced $($zipExe.Name) with signed binary." - } else { - Write-Host " No signed match found for $($zipExe.Name)." - } - } + run: pnpm exec electron-builder --config ${{ matrix.config }} --prepackaged build/${{ matrix.win_unpacked_dir }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SUPABASE_URL: ${{ secrets.SUPABASE_URL }} + SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} + APPINSIGHTS_CONNECTION_STRING: ${{ secrets.APPINSIGHTS_CONNECTION_STRING }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} - Remove-Item $zip.FullName -Force - Compress-Archive -Path (Join-Path $tempDir '*') -DestinationPath $zip.FullName -Force - } + - name: Sign Windows installers with Azure Trusted Signing + if: matrix.os == 'windows-latest' + uses: azure/artifact-signing-action@v1 + with: + azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} + azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} + azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} + endpoint: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }} + signing-account-name: ${{ secrets.TRUSTED_SIGNING_ACCOUNT_NAME }} + certificate-profile-name: ${{ secrets.TRUSTED_SIGNING_CERTIFICATE_PROFILE }} + files-folder: ${{ github.workspace }}/build + files-folder-filter: exe,msi + files-folder-recurse: false + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + description: Power Platform ToolBox + description-url: https://github.com/PowerPlatformToolBox/desktop-app - name: Regenerate latest.yml with correct SHA256 hashes (Windows) if: matrix.os == 'windows-latest' diff --git a/docs/azure-trusted-signing.md b/docs/azure-trusted-signing.md index 0e620bce..82cc9c2f 100644 --- a/docs/azure-trusted-signing.md +++ b/docs/azure-trusted-signing.md @@ -25,16 +25,37 @@ This document explains how Windows artifacts generated by the production and nig ## Workflow behavior -- The jobs running on `windows-latest` in both `prod-release.yml` and `nightly-release.yml` execute the [`azure/artifact-signing-action@v1`](https://github.com/Azure/artifact-signing-action) step immediately after Electron Builder packages the installers. -- Every `.exe` and `.msi` produced under `build/` is signed in place. Portable `.zip` artifacts remain unsigned because the action does not yet support signing archives. The action handles hashing, timestamping, and certificate management by requesting a short-lived certificate from Azure Trusted Signing. -- The action fails if any secret is missing or Azure denies the signing request, which prevents unsigned installers from being uploaded or released. +The Windows signing process uses a **three-phase signing approach** to ensure every binary—both the application itself and the installer wrappers—carries a valid signature: + +### Phase 1 – Sign application binaries + +After building only the unpacked application directory (`build/win-unpacked/` for x64, `build/win-arm64-unpacked/` for arm64), the `azure/artifact-signing-action@v1` step signs all `.exe` and `.dll` files found recursively inside that directory. This includes: + +- `Power Platform ToolBox.exe` (the main application executable) +- All `.dll` files bundled with the application + +### Phase 2 – Package installers from signed binaries + +With the application binaries now signed, Electron Builder is invoked with `--prepackaged` pointing at the already-signed directory. This creates the NSIS installer (`.exe`), MSI (`.msi`), MSI-wrapped EXE (`msiWrapped`), and portable ZIP, all of which embed the **signed** application binaries. + +### Phase 3 – Sign installer packages + +A second `azure/artifact-signing-action@v1` step signs the top-level installer files in `build/` (non-recursive) so that the outer wrappers are also authenticated: + +- `Power-Platform-ToolBox-*-win.exe` (NSIS setup installer) +- `Power-Platform-ToolBox-*-win.msi` (MSI installer) +- `Power-Platform-ToolBox-*-Setup.exe` (msiWrapped EXE bootstrapper, x64 only) + +Portable `.zip` archives are not signed as packages (the action does not support archive signing), but they contain the signed application binaries from Phase 1 because the ZIP is assembled from the `--prepackaged` directory. + +The action fails if any secret is missing or Azure denies the signing request, which prevents unsigned installers from being uploaded or released. ## Testing the signing step 1. Configure the secrets above in the repository or organization settings. 2. Trigger the `Stable Release` or `Insider Pre-Release` workflow via the `workflow_dispatch` entry point (you can do this on a throwaway branch after updating the version and release notes). -3. Inspect the Windows job logs for the "Sign Windows artifacts with Azure Trusted Signing" step to verify that Azure returned a certificate and that all `.exe`/`.msi` files were processed. -4. Download the Windows artifacts and run `Get-AuthenticodeSignature` locally to validate the signature chain. +3. Inspect the Windows job logs for the "Sign application binaries with Azure Trusted Signing (Windows)" and "Sign Windows installers with Azure Trusted Signing" steps to verify that Azure returned a certificate and that all files were processed. +4. Download the Windows artifacts and run `Get-AuthenticodeSignature` locally to validate the signature chain on both the installer and the installed application executable. ## Operational guidance