From 948e6637fba77783e3848fdeff4760a57bf2a091 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Mon, 19 Oct 2026 00:32:44 +0800 Subject: [PATCH] Sign Windows release builds with the SignPath release certificate Release-branch builds sign every unsigned executable, DLL and Python module in the x64 and arm64 portable bundles and installers, after approval in SignPath. The binaries and installer now carry OrcaSlicer version information. --- .github/workflows/build_orca.yml | 125 ++++++++++-------- CMakeLists.txt | 12 ++ scripts/restore-from-signpath.ps1 | 40 ++++++ scripts/stage-for-signpath.ps1 | 81 ++++++++++++ scripts/verify-authenticode.ps1 | 34 +++-- signpath/README.md | 21 ++- signpath/windows-binaries-v1.xml | 25 ++++ signpath/windows-installer-v1.xml | 5 +- signpath/windows-portable-v1.xml | 12 -- src/CMakeLists.txt | 8 +- .../platform/msw/OrcaSlicer-version.rc.in | 26 ++++ src/dev-utils/platform/msw/OrcaSlicer.rc.in | 25 +--- 12 files changed, 298 insertions(+), 116 deletions(-) create mode 100644 scripts/restore-from-signpath.ps1 create mode 100644 scripts/stage-for-signpath.ps1 create mode 100644 signpath/windows-binaries-v1.xml delete mode 100644 signpath/windows-portable-v1.xml create mode 100644 src/dev-utils/platform/msw/OrcaSlicer-version.rc.in diff --git a/.github/workflows/build_orca.yml b/.github/workflows/build_orca.yml index 1948653473..93f77ad539 100644 --- a/.github/workflows/build_orca.yml +++ b/.github/workflows/build_orca.yml @@ -424,6 +424,10 @@ jobs: "BUILD_DIR=build" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8 "ARCH_SUFFIX=_x64" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8 } + # Release branches ship binaries and installers signed through SignPath with the + # release certificate. Every signing request waits for a manual approval in + # SignPath, so main and pull request builds stay unsigned. + "SIGN_WINDOWS=${{ github.repository == 'OrcaSlicer/OrcaSlicer' && startsWith(github.ref, 'refs/heads/release/') && !vars.SELF_HOSTED }}" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8 - name: setup MSVC if: runner.os == 'Windows' @@ -504,100 +508,117 @@ jobs: retention-days: 5 if-no-files-found: error - - name: Upload unsigned Windows portable artifact for SignPath - id: upload-windows-portable - if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'Windows' && !vars.SELF_HOSTED + # Signs OrcaSlicer's own binaries and every bundled executable, DLL and Python + # extension module that ships without a signature; files their vendor signed, such + # as the Microsoft runtime, keep that signature. See signpath/README.md. + - name: Stage Windows binaries for SignPath + id: stage-windows-binaries + if: env.SIGN_WINDOWS == 'true' + shell: pwsh + run: | + $version = "${{ env.ver }}" -replace '^V', '' + ./scripts/stage-for-signpath.ps1 -Bundle "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer" -Destination "${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/unsigned-binaries" -ProductVersion $version + "version=$version" | Out-File -Append -FilePath $env:GITHUB_OUTPUT -Encoding utf8 + + - name: Upload unsigned Windows binaries for SignPath + id: upload-windows-binaries + if: env.SIGN_WINDOWS == 'true' uses: actions/upload-artifact@v7 with: - name: OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable_unsigned - path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer + name: signpath_windows_binaries${{ env.ARCH_SUFFIX }}_unsigned + path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/unsigned-binaries + retention-days: 1 if-no-files-found: error - - name: Submit Windows portable artifact to SignPath - if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'Windows' && !vars.SELF_HOSTED + # Waits until the request is approved in SignPath. + - name: Sign Windows binaries with SignPath + if: env.SIGN_WINDOWS == 'true' uses: signpath/github-action-submit-signing-request@v2 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }} project-slug: OrcaSlicer - signing-policy-slug: test-signing - artifact-configuration-slug: windows-portable-v1 - github-artifact-id: ${{ steps.upload-windows-portable.outputs.artifact-id }} + signing-policy-slug: release-signing + artifact-configuration-slug: windows-binaries-v1 + github-artifact-id: ${{ steps.upload-windows-binaries.outputs.artifact-id }} + parameters: | + version: "${{ steps.stage-windows-binaries.outputs.version }}" wait-for-completion: true - output-artifact-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/windows-portable + wait-for-completion-timeout-in-seconds: 10800 + output-artifact-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-binaries - - name: Verify SignPath Windows portable signatures - if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'Windows' && !vars.SELF_HOSTED + - name: Verify Windows binary signatures + if: env.SIGN_WINDOWS == 'true' shell: pwsh - # -AllowUntrustedRoot is required while signing with the SignPath test - # certificate (self-signed). Remove it once signing-policy-slug switches - # to release-signing with a production CA-issued certificate. - run: ./scripts/verify-authenticode.ps1 -ArtifactDirectory '${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/windows-portable' -AllowUntrustedRoot + run: ./scripts/verify-authenticode.ps1 -ArtifactDirectory '${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-binaries' - name: Replace Windows binaries with signed output - if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'Windows' && !vars.SELF_HOSTED + if: env.SIGN_WINDOWS == 'true' shell: pwsh - run: | - $source = Join-Path "${{ github.workspace }}" "${{ env.BUILD_DIR }}/signpath/windows-portable" - $destination = Join-Path "${{ github.workspace }}" "${{ env.BUILD_DIR }}/OrcaSlicer" - if (-not (Test-Path -LiteralPath $source -PathType Container)) { - throw "SignPath output directory not found: $source" - } - Get-ChildItem -LiteralPath $source -Force | Copy-Item -Destination $destination -Recurse -Force - - # cpack's install rules take the binaries from the build output, not from - # the portable bundle, so the installer needs the signed copies there too. - $buildOutput = Join-Path "${{ github.workspace }}" "${{ env.BUILD_DIR }}/src/Release" - if (-not (Test-Path -LiteralPath $buildOutput -PathType Container)) { - throw "Build output directory not found: $buildOutput" - } - foreach ($name in "orca-slicer.exe", "OrcaSlicer.dll") { - Copy-Item -LiteralPath (Join-Path $source $name) -Destination $buildOutput -Force - } + run: ./scripts/restore-from-signpath.ps1 -SignedDirectory "${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-binaries" -Bundle "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer" # NSIS is x86-only; it runs (and the installer it emits runs) under ARM64's # x86 emulation, packaging the native arm64 payload from build-arm64. + # The installer packs the installed OrcaSlicer/ tree, which the portable zip and the + # MSIX are made from too, instead of re-running the install rules: those take + # CPython and uv from the dependency tree and the download cache, which keep the + # unsigned copies. - name: Create installer Win if: runner.os == 'Windows' && !vars.SELF_HOSTED working-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }} run: | - cpack -G NSIS + $bundle = "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer".Replace('\', '/') + cpack -G NSIS -D CPACK_INSTALL_CMAKE_PROJECTS= -D "CPACK_INSTALLED_DIRECTORIES=$bundle;." + + # cpack stages what the installer packs under _CPack_Packages//NSIS/; + # every executable and DLL there must carry a valid signature, ours or its vendor's. + - name: Verify every binary the installer packs is signed + if: env.SIGN_WINDOWS == 'true' + shell: pwsh + run: | + $payload = Get-Item "${{ github.workspace }}/${{ env.BUILD_DIR }}/_CPack_Packages/*/NSIS/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}" + ./scripts/verify-authenticode.ps1 -ArtifactDirectory $payload.FullName + + - name: Stage Windows installer for SignPath + if: env.SIGN_WINDOWS == 'true' + shell: pwsh + run: ./scripts/stage-for-signpath.ps1 -Path "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe" -Destination "${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/unsigned-installer" -ProductVersion "${{ steps.stage-windows-binaries.outputs.version }}" - name: Upload unsigned Windows installer for SignPath id: upload-windows-installer - if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'Windows' && !vars.SELF_HOSTED + if: env.SIGN_WINDOWS == 'true' uses: actions/upload-artifact@v7 with: - name: OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}_unsigned - path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe + name: signpath_windows_installer${{ env.ARCH_SUFFIX }}_unsigned + path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/unsigned-installer + retention-days: 1 if-no-files-found: error - - name: Submit Windows installer to SignPath - if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'Windows' && !vars.SELF_HOSTED + - name: Sign Windows installer with SignPath + if: env.SIGN_WINDOWS == 'true' uses: signpath/github-action-submit-signing-request@v2 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }} project-slug: OrcaSlicer - signing-policy-slug: test-signing + signing-policy-slug: release-signing artifact-configuration-slug: windows-installer-v1 github-artifact-id: ${{ steps.upload-windows-installer.outputs.artifact-id }} + parameters: | + version: "${{ steps.stage-windows-binaries.outputs.version }}" wait-for-completion: true - output-artifact-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/windows-installer + wait-for-completion-timeout-in-seconds: 3600 + output-artifact-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-installer - - name: Verify SignPath Windows installer signature - if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'Windows' && !vars.SELF_HOSTED + - name: Verify Windows installer signature + if: env.SIGN_WINDOWS == 'true' shell: pwsh - # -AllowUntrustedRoot: see "Verify SignPath Windows portable signatures". - run: ./scripts/verify-authenticode.ps1 -ArtifactDirectory '${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/windows-installer' -Files 'OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe' -AllowUntrustedRoot + run: ./scripts/verify-authenticode.ps1 -ArtifactDirectory '${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-installer' -Files 'OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe' - name: Replace Windows installer with signed output - if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'Windows' && !vars.SELF_HOSTED + if: env.SIGN_WINDOWS == 'true' shell: pwsh - run: | - $name = "OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe" - $source = Join-Path "${{ github.workspace }}" "${{ env.BUILD_DIR }}/signpath/windows-installer/$name" - Copy-Item -LiteralPath $source -Destination (Join-Path "${{ github.workspace }}" "${{ env.BUILD_DIR }}/$name") -Force + run: Copy-Item -LiteralPath "${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-installer/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe" -Destination "${{ github.workspace }}/${{ env.BUILD_DIR }}" -Force - name: Pack app if: runner.os == 'Windows' diff --git a/CMakeLists.txt b/CMakeLists.txt index a302bb15e2..5bea14e54a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1355,6 +1355,18 @@ set (CPACK_NSIS_EXECUTABLES_DIRECTORY ".") set(CPACK_PACKAGE_EXECUTABLES "orca-slicer;OrcaSlicer") set(CPACK_CREATE_DESKTOP_LINKS "orca-slicer") set (CPACK_RESOURCE_FILE_LICENSE ${CMAKE_SOURCE_DIR}/LICENSE.txt) # must also include in install command +# Version information of the installer, the same as the binaries it installs carry +# (src/dev-utils/platform/msw/OrcaSlicer-version.rc.in): SignPath only signs it when +# ProductName and ProductVersion match signpath/windows-installer-v1.xml. The NSIS strings +# are single-quoted because CPack writes this value into CPackConfig.cmake unescaped. +string (CONCAT CPACK_NSIS_DEFINES + "VIProductVersion '${ORCA_VERSION_MAJOR}.${ORCA_VERSION_MINOR}.${ORCA_VERSION_PATCH}.0'\n" + "VIFileVersion '${ORCA_VERSION_MAJOR}.${ORCA_VERSION_MINOR}.${ORCA_VERSION_PATCH}.0'\n" + "VIAddVersionKey /LANG=1033 'CompanyName' 'SoftFever'\n" + "VIAddVersionKey /LANG=1033 'FileDescription' '${SLIC3R_APP_NAME} Installer'\n" + "VIAddVersionKey /LANG=1033 'FileVersion' '${SoftFever_VERSION}'\n" + "VIAddVersionKey /LANG=1033 'ProductName' '${SLIC3R_APP_NAME}'\n" + "VIAddVersionKey /LANG=1033 'ProductVersion' '${SoftFever_VERSION}'\n") set(CPACK_WIX_UPGRADE_GUID "058245e8-20e0-4a95-9ab7-1acfe17ad511") set(CPACK_GENERATOR NSIS) diff --git a/scripts/restore-from-signpath.ps1 b/scripts/restore-from-signpath.ps1 new file mode 100644 index 0000000000..e53e7d652f --- /dev/null +++ b/scripts/restore-from-signpath.ps1 @@ -0,0 +1,40 @@ +# Copies the files SignPath signed back over the originals in the portable bundle they +# were staged from with stage-for-signpath.ps1 -Bundle: the root of the signed artifact +# holds OrcaSlicer's own binaries, third-party/ the other files under their paths in the +# bundle. +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })] + [string]$SignedDirectory, + + [Parameter(Mandatory = $true)] + [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })] + [string]$Bundle +) + +$ErrorActionPreference = "Stop" + +$signedRoot = (Resolve-Path -LiteralPath $SignedDirectory).Path.TrimEnd('\', '/') +$thirdPartyPrefix = "third-party" + [IO.Path]::DirectorySeparatorChar +$files = Get-ChildItem -LiteralPath $signedRoot -Recurse -File +if (-not $files) { + throw "No signed files found in '$signedRoot'." +} + +foreach ($file in $files) { + $relative = $file.FullName.Substring($signedRoot.Length + 1) + if ($relative.StartsWith($thirdPartyPrefix, [StringComparison]::OrdinalIgnoreCase)) { + $relative = $relative.Substring($thirdPartyPrefix.Length) + } + + # Every signed file replaces one that was staged, so a file without an original means + # the artifact does not have the layout stage-for-signpath.ps1 produces. + $target = Join-Path $Bundle $relative + if (-not (Test-Path -LiteralPath $target -PathType Leaf)) { + throw "Signed file '$relative' has no original in '$Bundle'." + } + Copy-Item -LiteralPath $file.FullName -Destination $target -Force +} + +Write-Host "Restored $($files.Count) signed files into '$Bundle'." diff --git a/scripts/stage-for-signpath.ps1 b/scripts/stage-for-signpath.ps1 new file mode 100644 index 0000000000..6636f468e3 --- /dev/null +++ b/scripts/stage-for-signpath.ps1 @@ -0,0 +1,81 @@ +# Copies the files a SignPath signing request covers into the directory that request +# uploads, after checking their version information. SignPath refuses to sign a file +# whose ProductName or ProductVersion differs from its artifact configuration (see +# signpath/), and reports it only once the request has been approved, so the check runs +# before anything is submitted. +# +# -Path stages the given files at the root of -Destination. +# +# -Bundle stages a portable OrcaSlicer bundle for signpath/windows-binaries-v1.xml: +# OrcaSlicer's own binaries (-OwnBinaries) at the root, where their version information +# is checked, and every other executable, DLL and Python extension module that carries no +# signature under third-party/, at its path in the bundle. Files signed by their vendor, +# such as the Microsoft runtime, keep that signature. restore-from-signpath.ps1 copies the +# signed files back. +[CmdletBinding(DefaultParameterSetName = "Path")] +param( + [Parameter(Mandatory = $true, ParameterSetName = "Path")] + [string[]]$Path, + + [Parameter(Mandatory = $true, ParameterSetName = "Bundle")] + [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })] + [string]$Bundle, + + [Parameter(ParameterSetName = "Bundle")] + [string[]]$OwnBinaries = @("orca-slicer.exe", "OrcaSlicer.dll"), + + [Parameter(Mandatory = $true)] + [string]$Destination, + + [Parameter(Mandatory = $true)] + [string]$ProductVersion, + + [string]$ProductName = "OrcaSlicer" +) + +$ErrorActionPreference = "Stop" + +function Test-VersionInfo { + param([string]$File) + + if (-not (Test-Path -LiteralPath $File -PathType Leaf)) { + throw "File to sign was not found: $File" + } + + $info = [Diagnostics.FileVersionInfo]::GetVersionInfo((Resolve-Path -LiteralPath $File).Path) + if ($info.ProductName -ne $ProductName -or $info.ProductVersion -ne $ProductVersion) { + throw "'$File' has ProductName '$($info.ProductName)' and ProductVersion '$($info.ProductVersion)'; SignPath expects '$ProductName' and '$ProductVersion'." + } +} + +New-Item -ItemType Directory -Force -Path $Destination | Out-Null + +if ($PSCmdlet.ParameterSetName -eq "Path") { + foreach ($file in $Path) { + Test-VersionInfo $file + Copy-Item -LiteralPath $file -Destination $Destination -Force + Write-Host "Staged $(Split-Path -Leaf $file) ($ProductName $ProductVersion)" + } + exit 0 +} + +$bundleRoot = (Resolve-Path -LiteralPath $Bundle).Path.TrimEnd('\', '/') +foreach ($name in $OwnBinaries) { + $file = Join-Path $bundleRoot $name + Test-VersionInfo $file + Copy-Item -LiteralPath $file -Destination $Destination -Force + Write-Host "Staged $name ($ProductName $ProductVersion)" +} + +$thirdParty = Get-ChildItem -LiteralPath $bundleRoot -Recurse -File | + Where-Object { $_.Extension -in ".exe", ".dll", ".pyd" } | + Where-Object { $_.DirectoryName -ne $bundleRoot -or $_.Name -notin $OwnBinaries } | + Where-Object { (Get-AuthenticodeSignature -LiteralPath $_.FullName).Status -eq "NotSigned" } + +foreach ($file in $thirdParty) { + $relative = $file.FullName.Substring($bundleRoot.Length + 1) + $target = Join-Path (Join-Path $Destination "third-party") $relative + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $target) | Out-Null + Copy-Item -LiteralPath $file.FullName -Destination $target -Force +} +Write-Host "Staged $(@($thirdParty).Count) unsigned third-party files under third-party/" diff --git a/scripts/verify-authenticode.ps1 b/scripts/verify-authenticode.ps1 index 9edb91d057..d5d12ce6d2 100644 --- a/scripts/verify-authenticode.ps1 +++ b/scripts/verify-authenticode.ps1 @@ -4,17 +4,15 @@ param( [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })] [string]$ArtifactDirectory, - [string[]]$Files = @( - "orca-slicer.exe", - "OrcaSlicer.dll" - ), + # Paths relative to -ArtifactDirectory. Without them every executable, DLL and Python + # extension module under it is verified. + [string[]]$Files, [string]$SignToolPath, # Accept signatures whose certificate chain terminates in an untrusted root. # Required for the SignPath test certificate (self-signed). Do NOT pass this - # once a production CA-issued certificate is in use, so release builds enforce - # a fully trusted chain. + # with the release certificate, so release builds enforce a fully trusted chain. [switch]$AllowUntrustedRoot ) @@ -57,11 +55,20 @@ function Resolve-SignToolPath { throw "signtool.exe was not found. Install the Windows SDK or pass -SignToolPath." } -$artifactRoot = (Resolve-Path -LiteralPath $ArtifactDirectory).Path +$artifactRoot = (Resolve-Path -LiteralPath $ArtifactDirectory).Path.TrimEnd('\', '/') $signtool = Resolve-SignToolPath -ExplicitPath $SignToolPath +if (-not $Files) { + $Files = Get-ChildItem -LiteralPath $artifactRoot -Recurse -File | + Where-Object { $_.Extension -in ".exe", ".dll", ".pyd" } | + ForEach-Object { $_.FullName.Substring($artifactRoot.Length + 1) } + if (-not $Files) { + throw "No executables or DLLs found in '$artifactRoot'." + } +} + Write-Host "Using SignTool: $signtool" -Write-Host "Verifying Authenticode signatures in: $artifactRoot" +Write-Host "Verifying Authenticode signatures of $(@($Files).Count) files in: $artifactRoot" foreach ($relativePath in $Files) { $filePath = Join-Path $artifactRoot $relativePath @@ -69,8 +76,6 @@ foreach ($relativePath in $Files) { throw "Expected signed file was not found: $filePath" } - Write-Host "Verifying $relativePath" - # Capture signtool output without letting native stderr (redirected via 2>&1) # raise a terminating NativeCommandError under $ErrorActionPreference = 'Stop'. $previousErrorActionPreference = $ErrorActionPreference @@ -83,9 +88,9 @@ foreach ($relativePath in $Files) { $ErrorActionPreference = $previousErrorActionPreference } - $output | ForEach-Object { Write-Host $_ } - + $signature = Get-AuthenticodeSignature -LiteralPath $filePath if ($exitCode -eq 0) { + Write-Host "Verified $relativePath ($($signature.SignerCertificate.Subject))" continue } @@ -95,15 +100,16 @@ foreach ($relativePath in $Files) { # structured Get-AuthenticodeSignature result instead: accept only when the # file is genuinely signed and the sole problem is that the chain terminates # in an untrusted root (i.e. the self-signed SignPath test certificate). - $signature = Get-AuthenticodeSignature -LiteralPath $filePath $isSigned = ($signature.SignatureType -eq "Authenticode") -and ($null -ne $signature.SignerCertificate) $untrustedRootOnly = $signature.StatusMessage -match "terminated in a root certificate which is not trusted" if ($isSigned -and $untrustedRootOnly) { - Write-Host " Accepted: '$relativePath' is signed but its certificate chains to an untrusted root (expected for the SignPath test certificate)." + Write-Host "Accepted $relativePath ($($signature.SignerCertificate.Subject)): signed, but its certificate chains to an untrusted root (expected for the SignPath test certificate)." continue } } + # The full chain and error details only for a file that did not verify. + $output | ForEach-Object { Write-Host " $_" } throw "SignTool verification failed for '$relativePath' with exit code $exitCode." } diff --git a/signpath/README.md b/signpath/README.md index 91def052ee..fb22f56195 100644 --- a/signpath/README.md +++ b/signpath/README.md @@ -1,22 +1,21 @@ # SignPath configurations -This directory contains SignPath artifact configurations used by GitHub Actions. +Windows builds of `release/*` branches in `OrcaSlicer/OrcaSlicer` are code signed through [SignPath](https://about.signpath.io/), with the certificate the SignPath Foundation issues to open source projects. The XML files here are copies of the project's artifact configurations in SignPath, which the workflow refers to by slug only; a change to one has to be made in SignPath too, under the same slug. -## `windows-portable-v1` +Every signing request uses the `release-signing` policy and waits in SignPath until it is approved, so each Windows build of a release branch needs two approvals per architecture: the binaries, then the installer. Builds of `main` and pull requests are not signed. -`windows-portable-v1.xml` is the initial conservative Windows portable-bundle signing configuration. It signs only the two first-party binaries: +## What gets signed -- `orca-slicer.exe` -- `OrcaSlicer.dll` +Every executable, DLL and Python extension module in the bundle ends up with a valid signature, so Windows features that check each loaded binary, such as Smart App Control, find none unsigned. Files their vendor already signed keep that signature: the Microsoft runtime DLLs and `WebView2Loader.dll` (Microsoft) and CPython's bundled OpenSSL and libffi (Python Software Foundation). Everything else is signed with the project's certificate: OrcaSlicer's own `orca-slicer.exe` and `OrcaSlicer.dll`, the libraries built from upstream sources (OCCT, FFmpeg, FreeType, GMP, MPFR, CPython), the downloaded `uv.exe`, and the NSIS installer. -Do not broaden this to all DLLs without first confirming ownership, provenance, and whether upstream vendor signatures should be verified instead. +The SignPath Foundation's conditions ask projects to sign only binaries built from their own source and to leave upstream projects' binaries unsigned, which they allow inside a signed installer. Signing the upstream libraries departs from that, by the maintainers' choice. Narrowing the signed set back to OrcaSlicer's own files means dropping the `third-party` file set from `windows-binaries-v1` and the third-party staging from `scripts/stage-for-signpath.ps1`. -The Windows workflow uploads `${{ env.BUILD_DIR }}/OrcaSlicer` with `actions/upload-artifact`. GitHub stores that artifact as a ZIP, and the uploaded directory contents are rooted at the ZIP root. Because of that, the SignPath configuration uses `` with `orca-slicer.exe` and `OrcaSlicer.dll` directly beneath it. +The Foundation also requires metadata restrictions on signed files. OrcaSlicer's own binaries and the installer must have the ProductName `OrcaSlicer` and the ProductVersion passed in with the request, which the workflow takes from `SoftFever_VERSION` in `version.inc`. The binaries get that version information from `src/dev-utils/platform/msw/OrcaSlicer-version.rc.in`, the installer from `CPACK_NSIS_DEFINES` in `CMakeLists.txt`. The upstream files carry their own projects' metadata, so their file set has no restriction. `scripts/stage-for-signpath.ps1` checks the version information before a request is submitted, since SignPath only rejects a mismatch after the request has been approved. -The release portable ZIP is a separate archive created with 7-Zip from `${{ env.BUILD_DIR }}/OrcaSlicer`; that archive keeps the top-level `OrcaSlicer/` folder. After SignPath returns the signed artifact, the workflow copies the signed files back into `${{ env.BUILD_DIR }}/OrcaSlicer` and recreates the portable release ZIP so the public ZIP layout stays unchanged. It also copies them over the build output in `${{ env.BUILD_DIR }}/src/Release`, because `cpack` packages the installer from there rather than from the portable bundle. +## `windows-binaries-v1` + +Signs the binaries of the installed bundle, `${{ env.BUILD_DIR }}/OrcaSlicer`. `scripts/stage-for-signpath.ps1 -Bundle` stages them for upload: `orca-slicer.exe` and `OrcaSlicer.dll` at the root of the artifact ZIP, where the metadata restrictions apply, and every other unsigned `.exe`, `.dll` and `.pyd` under `third-party/`, at its path in the bundle. `scripts/restore-from-signpath.ps1` copies the signed files back over the originals. The portable ZIP, the MSIX package and the installer are all packed from that bundle; `cpack` is pointed at it with `CPACK_INSTALLED_DIRECTORIES` rather than re-running the install rules, which would take CPython and `uv.exe` from their unsigned sources again. A check after `cpack` verifies that every binary in the installer's payload is signed. ## `windows-installer-v1` -`windows-installer-v1.xml` signs the NSIS installer that `cpack` produces, `OrcaSlicer_Windows_Installer__.exe`. The workflow uploads that single file, so it sits at the root of the artifact ZIP. The installer is built after the binaries are signed, so the files it installs carry the `windows-portable-v1` signatures. - -Both configurations must also be created in the SignPath project under the same slugs; the workflow refers to them only by slug. +Signs the NSIS installer `cpack` produces, `OrcaSlicer_Windows_Installer__.exe`, uploaded as the only file of its artifact. The uninstaller NSIS writes during installation is not signed. diff --git a/signpath/windows-binaries-v1.xml b/signpath/windows-binaries-v1.xml new file mode 100644 index 0000000000..7c353dad83 --- /dev/null +++ b/signpath/windows-binaries-v1.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/signpath/windows-installer-v1.xml b/signpath/windows-installer-v1.xml index ec70556116..725f35ab06 100644 --- a/signpath/windows-installer-v1.xml +++ b/signpath/windows-installer-v1.xml @@ -1,7 +1,10 @@ + + + - + diff --git a/signpath/windows-portable-v1.xml b/signpath/windows-portable-v1.xml deleted file mode 100644 index 5d5b717ffa..0000000000 --- a/signpath/windows-portable-v1.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c8d882f291..697aede16f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -140,7 +140,11 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/dev-utils/platform/msw/OrcaSlicer.rc. configure_file(${CMAKE_CURRENT_SOURCE_DIR}/dev-utils/platform/msw/OrcaSlicer.manifest.in ${CMAKE_CURRENT_BINARY_DIR}/OrcaSlicer.manifest @ONLY) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/dev-utils/platform/osx/Info.plist.in ${CMAKE_CURRENT_BINARY_DIR}/Info.plist @ONLY) if (WIN32) - add_library(OrcaSlicer SHARED OrcaSlicer.cpp OrcaSlicer.hpp) + foreach (_orca_rc_binary IN ITEMS orca-slicer.exe OrcaSlicer.dll) + set(ORCA_RC_ORIGINAL_FILENAME ${_orca_rc_binary}) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/dev-utils/platform/msw/OrcaSlicer-version.rc.in ${CMAKE_CURRENT_BINARY_DIR}/${_orca_rc_binary}.version.rc @ONLY) + endforeach () + add_library(OrcaSlicer SHARED OrcaSlicer.cpp OrcaSlicer.hpp ${CMAKE_CURRENT_BINARY_DIR}/OrcaSlicer.dll.version.rc) else () add_executable(OrcaSlicer OrcaSlicer.cpp OrcaSlicer.hpp) endif () @@ -216,7 +220,7 @@ if (WIN32) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -municode") endif() - add_executable(OrcaSlicer_app_gui WIN32 OrcaSlicer_app_msvc.cpp ${CMAKE_CURRENT_BINARY_DIR}/OrcaSlicer.rc) + add_executable(OrcaSlicer_app_gui WIN32 OrcaSlicer_app_msvc.cpp ${CMAKE_CURRENT_BINARY_DIR}/OrcaSlicer.rc ${CMAKE_CURRENT_BINARY_DIR}/orca-slicer.exe.version.rc) # Generate debug symbols even in release mode. if(MSVC) target_link_options(OrcaSlicer_app_gui PUBLIC "$<$:/DEBUG>") diff --git a/src/dev-utils/platform/msw/OrcaSlicer-version.rc.in b/src/dev-utils/platform/msw/OrcaSlicer-version.rc.in new file mode 100644 index 0000000000..06fec8f366 --- /dev/null +++ b/src/dev-utils/platform/msw/OrcaSlicer-version.rc.in @@ -0,0 +1,26 @@ +// Version information of orca-slicer.exe and OrcaSlicer.dll, configured once per binary. +// SignPath only signs a binary whose ProductName and ProductVersion match its artifact +// configuration (signpath/windows-binaries-v1.xml), which the workflow passes the +// SoftFever_VERSION from version.inc. +1 VERSIONINFO +FILEVERSION @ORCA_VERSION_MAJOR@,@ORCA_VERSION_MINOR@,@ORCA_VERSION_PATCH@,0 +PRODUCTVERSION @ORCA_VERSION_MAJOR@,@ORCA_VERSION_MINOR@,@ORCA_VERSION_PATCH@,0 +{ + BLOCK "StringFileInfo" + { + BLOCK "040904E4" + { + VALUE "CompanyName", "SoftFever" + VALUE "FileDescription", "@SLIC3R_APP_NAME@" + VALUE "FileVersion", "@SoftFever_VERSION@" + VALUE "ProductName", "@SLIC3R_APP_NAME@" + VALUE "ProductVersion", "@SoftFever_VERSION@" + VALUE "InternalName", "@SLIC3R_APP_NAME@" + VALUE "OriginalFilename", "@ORCA_RC_ORIGINAL_FILENAME@" + } + } + BLOCK "VarFileInfo" + { + VALUE "Translation", 0x409, 1252 + } +} diff --git a/src/dev-utils/platform/msw/OrcaSlicer.rc.in b/src/dev-utils/platform/msw/OrcaSlicer.rc.in index 11df54b1b3..a98d152d14 100644 --- a/src/dev-utils/platform/msw/OrcaSlicer.rc.in +++ b/src/dev-utils/platform/msw/OrcaSlicer.rc.in @@ -1,25 +1,2 @@ -1 VERSIONINFO -FILEVERSION @SLIC3R_VERSION@ -PRODUCTVERSION @SLIC3R_VERSION@ -{ - BLOCK "StringFileInfo" - { - BLOCK "040904E4" - { - VALUE "CompanyName", "SoftFever" - VALUE "FileDescription", "@SLIC3R_APP_NAME@" - VALUE "FileVersion", "@SLIC3R_BUILD_ID@" - VALUE "ProductName", "@SLIC3R_APP_NAME@" - VALUE "ProductVersion", "@SLIC3R_BUILD_ID@" - VALUE "InternalName", "@SLIC3R_APP_NAME@" - VALUE "LegalCopyright", "" - VALUE "OriginalFilename", "orca-slicer.exe" - } - } - BLOCK "VarFileInfo" - { - VALUE "Translation", 0x409, 1252 - } -} 2 ICON "@SLIC3R_RESOURCES_DIR@/images/OrcaSlicer.ico" -1 24 "OrcaSlicer.manifest" +1 24 "OrcaSlicer.manifest"