diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml index 2c6eabf6d..4a51c4d94 100644 --- a/.github/workflows/desktop-build.yml +++ b/.github/workflows/desktop-build.yml @@ -224,6 +224,9 @@ jobs: environment: windows-signing env: MAPLE_WINDOWS_AUTHENTICODE_SUBJECT: ${{ secrets.AZURE_ARTIFACT_SIGNING_EXPECTED_SUBJECT }} + MAPLE_WINDOWS_ARTIFACT_SIGNING_ENDPOINT: ${{ secrets.AZURE_ARTIFACT_SIGNING_ENDPOINT }} + MAPLE_WINDOWS_ARTIFACT_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_ARTIFACT_SIGNING_ACCOUNT_NAME }} + MAPLE_WINDOWS_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME }} steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # was v4 with: @@ -280,26 +283,9 @@ jobs: tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - name: Sign Windows app executable - uses: Azure/artifact-signing-action@c0ae2c1d0c1847ab81ac0ab8521bee597cfedd30 # was v2 - with: - endpoint: ${{ secrets.AZURE_ARTIFACT_SIGNING_ENDPOINT }} - signing-account-name: ${{ secrets.AZURE_ARTIFACT_SIGNING_ACCOUNT_NAME }} - certificate-profile-name: ${{ secrets.AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME }} - files: ${{ github.workspace }}\frontend\src-tauri\target\release\maple.exe - file-digest: SHA256 - timestamp-rfc3161: http://timestamp.acs.microsoft.com - timestamp-digest: SHA256 - exclude-environment-credential: true - exclude-workload-identity-credential: true - exclude-managed-identity-credential: true - exclude-shared-token-cache-credential: true - exclude-visual-studio-credential: true - exclude-visual-studio-code-credential: true - exclude-azure-cli-credential: false - exclude-azure-powershell-credential: true - exclude-azure-developer-cli-credential: true - exclude-interactive-browser-credential: true + - name: Install Artifact Signing module + shell: pwsh + run: ./scripts/ci/install-windows-artifact-signing.ps1 - name: Bundle Windows installer shell: bash @@ -312,28 +298,6 @@ jobs: source scripts/ci/_common.sh setup_exe="$(windows_release_setup_exe_required)" echo "MAPLE_WINDOWS_SETUP_EXE_REL=$(repo_relative_path "${setup_exe}")" >> "${GITHUB_ENV}" - echo "MAPLE_WINDOWS_SETUP_EXE_WIN=$(to_windows_path "${setup_exe}")" >> "${GITHUB_ENV}" - - - name: Sign Windows installer - uses: Azure/artifact-signing-action@c0ae2c1d0c1847ab81ac0ab8521bee597cfedd30 # was v2 - with: - endpoint: ${{ secrets.AZURE_ARTIFACT_SIGNING_ENDPOINT }} - signing-account-name: ${{ secrets.AZURE_ARTIFACT_SIGNING_ACCOUNT_NAME }} - certificate-profile-name: ${{ secrets.AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME }} - files: ${{ env.MAPLE_WINDOWS_SETUP_EXE_WIN }} - file-digest: SHA256 - timestamp-rfc3161: http://timestamp.acs.microsoft.com - timestamp-digest: SHA256 - exclude-environment-credential: true - exclude-workload-identity-credential: true - exclude-managed-identity-credential: true - exclude-shared-token-cache-credential: true - exclude-visual-studio-credential: true - exclude-visual-studio-code-credential: true - exclude-azure-cli-credential: false - exclude-azure-powershell-credential: true - exclude-azure-developer-cli-credential: true - exclude-interactive-browser-credential: true - name: Finalize Windows updater signatures and checksums shell: bash diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 54a13283b..7a8cdb092 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -171,6 +171,9 @@ jobs: environment: windows-signing env: MAPLE_WINDOWS_AUTHENTICODE_SUBJECT: ${{ secrets.AZURE_ARTIFACT_SIGNING_EXPECTED_SUBJECT }} + MAPLE_WINDOWS_ARTIFACT_SIGNING_ENDPOINT: ${{ secrets.AZURE_ARTIFACT_SIGNING_ENDPOINT }} + MAPLE_WINDOWS_ARTIFACT_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_ARTIFACT_SIGNING_ACCOUNT_NAME }} + MAPLE_WINDOWS_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME }} permissions: contents: write id-token: write @@ -227,26 +230,9 @@ jobs: tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - name: Sign Windows app executable - uses: Azure/artifact-signing-action@c0ae2c1d0c1847ab81ac0ab8521bee597cfedd30 # was v2 - with: - endpoint: ${{ secrets.AZURE_ARTIFACT_SIGNING_ENDPOINT }} - signing-account-name: ${{ secrets.AZURE_ARTIFACT_SIGNING_ACCOUNT_NAME }} - certificate-profile-name: ${{ secrets.AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME }} - files: ${{ github.workspace }}\frontend\src-tauri\target\release\maple.exe - file-digest: SHA256 - timestamp-rfc3161: http://timestamp.acs.microsoft.com - timestamp-digest: SHA256 - exclude-environment-credential: true - exclude-workload-identity-credential: true - exclude-managed-identity-credential: true - exclude-shared-token-cache-credential: true - exclude-visual-studio-credential: true - exclude-visual-studio-code-credential: true - exclude-azure-cli-credential: false - exclude-azure-powershell-credential: true - exclude-azure-developer-cli-credential: true - exclude-interactive-browser-credential: true + - name: Install Artifact Signing module + shell: pwsh + run: ./scripts/ci/install-windows-artifact-signing.ps1 - name: Bundle Windows installer shell: bash @@ -259,28 +245,6 @@ jobs: source scripts/ci/_common.sh setup_exe="$(windows_release_setup_exe_required)" echo "MAPLE_WINDOWS_SETUP_EXE_REL=$(repo_relative_path "${setup_exe}")" >> "${GITHUB_ENV}" - echo "MAPLE_WINDOWS_SETUP_EXE_WIN=$(to_windows_path "${setup_exe}")" >> "${GITHUB_ENV}" - - - name: Sign Windows installer - uses: Azure/artifact-signing-action@c0ae2c1d0c1847ab81ac0ab8521bee597cfedd30 # was v2 - with: - endpoint: ${{ secrets.AZURE_ARTIFACT_SIGNING_ENDPOINT }} - signing-account-name: ${{ secrets.AZURE_ARTIFACT_SIGNING_ACCOUNT_NAME }} - certificate-profile-name: ${{ secrets.AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME }} - files: ${{ env.MAPLE_WINDOWS_SETUP_EXE_WIN }} - file-digest: SHA256 - timestamp-rfc3161: http://timestamp.acs.microsoft.com - timestamp-digest: SHA256 - exclude-environment-credential: true - exclude-workload-identity-credential: true - exclude-managed-identity-credential: true - exclude-shared-token-cache-credential: true - exclude-visual-studio-credential: true - exclude-visual-studio-code-credential: true - exclude-azure-cli-credential: false - exclude-azure-powershell-credential: true - exclude-azure-developer-cli-credential: true - exclude-interactive-browser-credential: true - name: Finalize Windows updater signatures and checksums shell: bash diff --git a/scripts/ci/_common.sh b/scripts/ci/_common.sh index 53290229b..4fbe9617b 100755 --- a/scripts/ci/_common.sh +++ b/scripts/ci/_common.sh @@ -3548,13 +3548,31 @@ windows_tauri_release_build_config() { } windows_tauri_release_bundle_config() { - jq -cn '{ + local sign_script + + sign_script="$(to_windows_path "${REPO_ROOT}/scripts/ci/windows-artifact-sign.ps1")" + + jq -cn --arg signScript "${sign_script}" '{ build: { beforeBuildCommand: null }, bundle: { createUpdaterArtifacts: false, - targets: ["nsis"] + targets: ["nsis"], + windows: { + signCommand: { + cmd: "pwsh", + args: [ + "-NoLogo", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + $signScript, + "%1" + ] + } + } } }' } diff --git a/scripts/ci/desktop-windows-release.sh b/scripts/ci/desktop-windows-release.sh index daaf962d1..f6b307308 100755 --- a/scripts/ci/desktop-windows-release.sh +++ b/scripts/ci/desktop-windows-release.sh @@ -12,13 +12,14 @@ build stage pinned runtime DLLs, and compile maple.exe without bundling. bundle - Verify maple.exe is Authenticode-signed, then generate the NSIS installer - from that signed app executable. The installer itself is not signed here. + Generate the NSIS installer. Tauri patches and signs maple.exe through + bundle.windows.signCommand during bundling, signs the installer, then restores + target/release/maple.exe to its original unsigned/unpatched bytes. finalize - Verify Authenticode signatures on maple.exe and the NSIS installer, create - the final Tauri updater signature for the signed installer, and emit release - reproducibility manifests. + Verify Authenticode signatures on the NSIS installer, create the final Tauri + updater signature for the signed installer, and emit release reproducibility + manifests. EOF } @@ -74,7 +75,10 @@ run_bundle_phase() { configure_reproducible_build_metadata app_exe="$(windows_release_app_exe)" - verify_windows_authenticode_signatures "${app_exe}" + if [ ! -f "${app_exe}" ]; then + echo "Windows app executable is missing before bundling: ${app_exe}" >&2 + exit 1 + fi cd "${FRONTEND_DIR}" remove_build_tree "${TAURI_DIR}/target/release/bundle/nsis" @@ -83,21 +87,24 @@ run_bundle_phase() { bun tauri bundle --verbose --bundles nsis --config "$(windows_tauri_release_bundle_config)" setup_exe="$(windows_release_setup_exe_required)" + # Tauri restores target/release/maple.exe after bundling. The durable signed + # artifact at this point is the NSIS installer; installed-payload verification + # should inspect an extracted installer payload, not the restored build output. + verify_windows_authenticode_signatures "${setup_exe}" print_file_hashes "${setup_exe}" verify_frontend_dist_unchanged } run_finalize_phase() { - local app_exe setup_exe repro_dir + local setup_exe repro_dir local windows_runtime_dlls=() print_source_provenance configure_reproducible_build_metadata configure_tauri_updater_signing_key - app_exe="$(windows_release_app_exe)" setup_exe="$(windows_release_setup_exe_required)" - verify_windows_authenticode_signatures "${app_exe}" "${setup_exe}" + verify_windows_authenticode_signatures "${setup_exe}" rm -f "${setup_exe}.sig" sign_tauri_updater_artifacts "${setup_exe}" diff --git a/scripts/ci/install-windows-artifact-signing.ps1 b/scripts/ci/install-windows-artifact-signing.ps1 new file mode 100644 index 000000000..f7a45e22c --- /dev/null +++ b/scripts/ci/install-windows-artifact-signing.ps1 @@ -0,0 +1,55 @@ +$ErrorActionPreference = "Stop" +Set-StrictMode -Version 3.0 + +$moduleName = "ArtifactSigning" +$moduleVersion = "0.1.8" +$moduleSha256 = "3221344b8c627915d3870f23e80816f31a5d8c2bae1d7c0cdd6c9652f6c4e089" +$moduleUrl = "https://www.powershellgallery.com/api/v2/package/$moduleName/$moduleVersion" + +if ([string]::IsNullOrWhiteSpace($env:RUNNER_TEMP)) { + $baseTemp = [System.IO.Path]::GetTempPath() +} else { + $baseTemp = $env:RUNNER_TEMP +} + +$moduleRoot = Join-Path $baseTemp "maple-powershell-modules" +$moduleDir = Join-Path $moduleRoot "$moduleName/$moduleVersion" +$downloadDir = Join-Path $baseTemp "maple-powershell-downloads" +$packagePath = Join-Path $downloadDir "$moduleName.$moduleVersion.nupkg" + +New-Item -ItemType Directory -Force -Path $downloadDir | Out-Null +New-Item -ItemType Directory -Force -Path $moduleRoot | Out-Null + +Invoke-WebRequest -Uri $moduleUrl -OutFile $packagePath -TimeoutSec 120 + +$actualSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $packagePath).Hash.ToLowerInvariant() +if ($actualSha256 -ne $moduleSha256) { + throw "$moduleName $moduleVersion hash mismatch. Expected $moduleSha256 but got $actualSha256." +} + +if (Test-Path -LiteralPath $moduleDir) { + Remove-Item -LiteralPath $moduleDir -Recurse -Force +} +New-Item -ItemType Directory -Force -Path $moduleDir | Out-Null +Expand-Archive -LiteralPath $packagePath -DestinationPath $moduleDir -Force + +$manifestPath = Join-Path $moduleDir "$moduleName.psd1" +Import-Module -Name $manifestPath -Force -ErrorAction Stop +$loadedModule = Get-Module $moduleName | Where-Object { $_.Version -eq [version]$moduleVersion } | Select-Object -First 1 +if (-not $loadedModule) { + throw "$moduleName $moduleVersion was not loaded from $manifestPath." +} +if (-not (Get-Command Invoke-ArtifactSigning -ErrorAction SilentlyContinue)) { + throw "Invoke-ArtifactSigning was not exported by $manifestPath." +} + +if (-not [string]::IsNullOrWhiteSpace($env:GITHUB_ENV)) { + "MAPLE_WINDOWS_ARTIFACT_SIGNING_MODULE_ROOT=$moduleRoot" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + if ([string]::IsNullOrWhiteSpace($env:PSModulePath)) { + "PSModulePath=$moduleRoot" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + } else { + "PSModulePath=$moduleRoot;$env:PSModulePath" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + } +} + +Write-Host "$moduleName $moduleVersion installed with verified SHA-256 $moduleSha256" diff --git a/scripts/ci/windows-artifact-sign.ps1 b/scripts/ci/windows-artifact-sign.ps1 new file mode 100644 index 000000000..e2232764d --- /dev/null +++ b/scripts/ci/windows-artifact-sign.ps1 @@ -0,0 +1,65 @@ +param( + [Parameter(Mandatory = $true)] + [string]$File +) + +$ArtifactSigningVersion = "0.1.8" + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version 3.0 + +if (-not (Test-Path -LiteralPath $File -PathType Leaf)) { + throw "Windows artifact to sign was not found: $File" +} + +$requiredEnv = @( + "MAPLE_WINDOWS_ARTIFACT_SIGNING_ENDPOINT", + "MAPLE_WINDOWS_ARTIFACT_SIGNING_ACCOUNT_NAME", + "MAPLE_WINDOWS_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME" +) + +foreach ($name in $requiredEnv) { + if ([string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($name))) { + throw "$name is required for Windows Artifact Signing." + } +} + +$moduleRoot = $env:MAPLE_WINDOWS_ARTIFACT_SIGNING_MODULE_ROOT +if (-not [string]::IsNullOrWhiteSpace($moduleRoot)) { + $moduleManifest = Join-Path $moduleRoot "ArtifactSigning/$ArtifactSigningVersion/ArtifactSigning.psd1" + if (-not (Test-Path -LiteralPath $moduleManifest -PathType Leaf)) { + throw "ArtifactSigning module manifest was not found: $moduleManifest" + } + Import-Module -Name $moduleManifest -Force -ErrorAction Stop +} else { + Import-Module ArtifactSigning -RequiredVersion $ArtifactSigningVersion -ErrorAction Stop +} + +$loadedModule = Get-Module ArtifactSigning | Where-Object { $_.Version -eq [version]$ArtifactSigningVersion } | Select-Object -First 1 +if (-not $loadedModule) { + throw "ArtifactSigning $ArtifactSigningVersion was not loaded." +} + +$params = @{ + Endpoint = $env:MAPLE_WINDOWS_ARTIFACT_SIGNING_ENDPOINT + CodeSigningAccountName = $env:MAPLE_WINDOWS_ARTIFACT_SIGNING_ACCOUNT_NAME + CertificateProfileName = $env:MAPLE_WINDOWS_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME + Files = $File + FileDigest = "SHA256" + TimestampRfc3161 = "http://timestamp.acs.microsoft.com" + TimestampDigest = "SHA256" + ExcludeEnvironmentCredential = $true + ExcludeWorkloadIdentityCredential = $true + ExcludeManagedIdentityCredential = $true + ExcludeSharedTokenCacheCredential = $true + ExcludeVisualStudioCredential = $true + ExcludeVisualStudioCodeCredential = $true + ExcludeAzureCliCredential = $false + ExcludeAzurePowerShellCredential = $true + ExcludeAzureDeveloperCliCredential = $true + ExcludeInteractiveBrowserCredential = $true +} + +Invoke-ArtifactSigning @params + +Write-Host ("signed-windows-artifact {0}" -f (Split-Path -Leaf $File))