Sign the uninstaller in Windows release installers

This commit is contained in:
SoftFever committed 2026-10-19 13:07:53 +08:00
1 parent 948e6637fb
commit 4c74ab393b
7 files changed
+97 -37

No files matched your search

+39 -11
View File
@@ -508,16 +508,30 @@ jobs:
retention-days: 5
if-no-files-found: error
# 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.
# makensis generates the uninstaller while it compiles the installer, so a first
# installer build exports it to be signed with the binaries, and "Create installer
# Win" embeds the signed copy. See src/dev-utils/platform/msw/uninstaller-signing.nsh.
- name: Export the Windows uninstaller for SignPath
if: env.SIGN_WINDOWS == 'true'
working-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}
shell: pwsh
run: |
$bundle = "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer".Replace('\', '/')
# Forward slashes: cpack drops the backslashes from makensis arguments.
$uninstaller = "${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/Uninstall.exe".Replace('\', '/')
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $uninstaller) | Out-Null
cpack -G NSIS -D CPACK_INSTALL_CMAKE_PROJECTS= -D "CPACK_INSTALLED_DIRECTORIES=$bundle;." -D "CPACK_NSIS_EXECUTABLE_PRE_ARGUMENTS=/DORCA_EXPORT_UNINSTALLER=$uninstaller"
# Signs OrcaSlicer's own binaries, the uninstaller 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
./scripts/stage-for-signpath.ps1 -Bundle "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer" -Path "${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/Uninstall.exe" -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
@@ -555,7 +569,7 @@ jobs:
- name: Replace Windows binaries with signed output
if: env.SIGN_WINDOWS == 'true'
shell: pwsh
run: ./scripts/restore-from-signpath.ps1 -SignedDirectory "${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-binaries" -Bundle "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer"
run: ./scripts/restore-from-signpath.ps1 -SignedDirectory "${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-binaries" -Bundle "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer" -Exclude Uninstall.exe
# 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.
@@ -568,16 +582,30 @@ jobs:
working-directory: ${{ github.workspace }}/${{ env.BUILD_DIR }}
run: |
$bundle = "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer".Replace('\', '/')
cpack -G NSIS -D CPACK_INSTALL_CMAKE_PROJECTS= -D "CPACK_INSTALLED_DIRECTORIES=$bundle;."
$signedUninstaller = @()
if ($env:SIGN_WINDOWS -eq 'true') {
$uninstaller = "${{ github.workspace }}/${{ env.BUILD_DIR }}/signpath/signed-binaries/Uninstall.exe".Replace('\', '/')
$signedUninstaller = "-D", "CPACK_NSIS_EXECUTABLE_PRE_ARGUMENTS=/DORCA_SIGNED_UNINSTALLER=$uninstaller"
}
cpack -G NSIS -D CPACK_INSTALL_CMAKE_PROJECTS= -D "CPACK_INSTALLED_DIRECTORIES=$bundle;." @signedUninstaller
# cpack stages what the installer packs under _CPack_Packages/<system>/NSIS/<package>;
# every executable and DLL there must carry a valid signature, ours or its vendor's.
- name: Verify every binary the installer packs is signed
# Installs into a scratch directory and checks that every executable and DLL an
# installation puts on disk, the uninstaller included, carries a valid signature,
# ours or its vendor's. The hosted runner is discarded afterwards, so the
# installation is left in place.
- name: Verify every binary the installer installs 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
$installDir = Join-Path "${{ runner.temp }}" "orca-install-check"
$installer = "${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe"
# NSIS requires /D= to come last.
$setup = Start-Process -FilePath $installer -ArgumentList "/S", "/D=$installDir" -Wait -PassThru
if ($setup.ExitCode -ne 0) { throw "The installer exited with code $($setup.ExitCode)." }
if (-not (Test-Path -LiteralPath (Join-Path $installDir "Uninstall.exe") -PathType Leaf)) {
throw "The installation in '$installDir' has no Uninstall.exe."
}
./scripts/verify-authenticode.ps1 -ArtifactDirectory $installDir
- name: Stage Windows installer for SignPath
if: env.SIGN_WINDOWS == 'true'
+10 -5
View File
@@ -1355,10 +1355,13 @@ 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.
# Version information of the installer and of the uninstaller it writes, the same as the
# binaries it installs carry (src/dev-utils/platform/msw/OrcaSlicer-version.rc.in):
# SignPath only signs them when ProductName and ProductVersion match the artifact
# configurations in signpath/. The include lets the release workflow embed a signed
# uninstaller. The NSIS strings are single-quoted and the paths use forward slashes
# because CPack writes this value into CPackConfig.cmake unescaped; !include only accepts
# a forward-slash path through !addincludedir.
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"
@@ -1366,7 +1369,9 @@ string (CONCAT CPACK_NSIS_DEFINES
"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")
"VIAddVersionKey /LANG=1033 'ProductVersion' '${SoftFever_VERSION}'\n"
"!addincludedir '${CMAKE_SOURCE_DIR}/src/dev-utils/platform/msw'\n"
"!include 'uninstaller-signing.nsh'\n")
set(CPACK_WIX_UPGRADE_GUID "058245e8-20e0-4a95-9ab7-1acfe17ad511")
set(CPACK_GENERATOR NSIS)
+8 -4
View File
@@ -1,7 +1,8 @@
# 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.
# bundle. Root files named in -Exclude came from outside the bundle, such as the exported
# uninstaller, and stay in -SignedDirectory.
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
@@ -10,14 +11,17 @@ param(
[Parameter(Mandatory = $true)]
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
[string]$Bundle
[string]$Bundle,
[string[]]$Exclude = @()
)
$ErrorActionPreference = "Stop"
$signedRoot = (Resolve-Path -LiteralPath $SignedDirectory).Path.TrimEnd('\', '/')
$thirdPartyPrefix = "third-party" + [IO.Path]::DirectorySeparatorChar
$files = Get-ChildItem -LiteralPath $signedRoot -Recurse -File
$files = Get-ChildItem -LiteralPath $signedRoot -Recurse -File |
Where-Object { $_.DirectoryName -ne $signedRoot -or $_.Name -notin $Exclude }
if (-not $files) {
throw "No signed files found in '$signedRoot'."
}
@@ -37,4 +41,4 @@ foreach ($file in $files) {
Copy-Item -LiteralPath $file.FullName -Destination $target -Force
}
Write-Host "Restored $($files.Count) signed files into '$Bundle'."
Write-Host "Restored $(@($files).Count) signed files into '$Bundle'."
+14 -13
View File
@@ -7,14 +7,16 @@
# -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
# OrcaSlicer's own binaries (-OwnBinaries, plus any -Path files from outside the bundle,
# such as the exported uninstaller) 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")]
[Parameter(ParameterSetName = "Bundle")]
[string[]]$Path,
[Parameter(Mandatory = $true, ParameterSetName = "Bundle")]
@@ -50,21 +52,20 @@ function Test-VersionInfo {
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
$ownFiles = @($Path | Where-Object { $_ })
if ($PSCmdlet.ParameterSetName -eq "Bundle") {
$bundleRoot = (Resolve-Path -LiteralPath $Bundle).Path.TrimEnd('\', '/')
$ownFiles = @($OwnBinaries | ForEach-Object { Join-Path $bundleRoot $_ }) + $ownFiles
}
$bundleRoot = (Resolve-Path -LiteralPath $Bundle).Path.TrimEnd('\', '/')
foreach ($name in $OwnBinaries) {
$file = Join-Path $bundleRoot $name
foreach ($file in $ownFiles) {
Test-VersionInfo $file
Copy-Item -LiteralPath $file -Destination $Destination -Force
Write-Host "Staged $name ($ProductName $ProductVersion)"
Write-Host "Staged $(Split-Path -Leaf $file) ($ProductName $ProductVersion)"
}
if ($PSCmdlet.ParameterSetName -eq "Path") {
exit 0
}
$thirdParty = Get-ChildItem -LiteralPath $bundleRoot -Recurse -File |
+6 -4
View File
@@ -6,16 +6,18 @@ Every signing request uses the `release-signing` policy and waits in SignPath un
## What gets signed
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.
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 with the `Uninstall.exe` it installs.
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 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 Foundation also requires metadata restrictions on signed files. OrcaSlicer's own binaries, the installer and the uninstaller 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 and its uninstaller 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.
## `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.
Signs the binaries of the installed bundle, `${{ env.BUILD_DIR }}/OrcaSlicer`, and the installer's uninstaller. `scripts/stage-for-signpath.ps1 -Bundle` stages them for upload: `orca-slicer.exe`, `OrcaSlicer.dll` and `Uninstall.exe` at the root of the artifact ZIP, where the metadata restrictions apply, and every other unsigned `.exe`, `.dll` and `.pyd` of the bundle 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.
makensis generates the uninstaller while it compiles the installer, so it cannot be signed after the fact like the other files. Before the binaries are submitted, the workflow builds the installer once to export its uninstaller, and the final `cpack` embeds the signed copy in its place, through the hooks in `src/dev-utils/platform/msw/uninstaller-signing.nsh`. Both builds pack the same files with the same script, so the uninstaller is the same one the final installer would have generated. After the final `cpack`, the workflow installs the installer silently into a scratch directory on the runner and verifies that every binary the installation puts on disk, the uninstaller included, carries a valid signature.
## `windows-installer-v1`
Signs the NSIS installer `cpack` produces, `OrcaSlicer_Windows_Installer_<version>_<arch>.exe`, uploaded as the only file of its artifact. The uninstaller NSIS writes during installation is not signed.
Signs the NSIS installer `cpack` produces, `OrcaSlicer_Windows_Installer_<version>_<arch>.exe`, uploaded as the only file of its artifact.
+1
View File
@@ -7,6 +7,7 @@
<pe-file-set product-name="OrcaSlicer" product-version="${version}">
<include path="orca-slicer.exe" />
<include path="OrcaSlicer.dll" />
<include path="Uninstall.exe" />
<for-each>
<authenticode-sign />
</for-each>
@@ -0,0 +1,19 @@
; Lets the Windows release workflow ship a signed uninstaller (see signpath/README.md).
; makensis generates the uninstaller while it compiles the installer, so it cannot be
; signed afterwards like the installed binaries. Instead a first compile exports it
; (/DORCA_EXPORT_UNINSTALLER=<path>), the export is signed together with the binaries, and
; the final compile embeds the signed copy in place of the uninstaller it generates
; (/DORCA_SIGNED_UNINSTALLER=<path>). Both compiles run the same script over the same
; installed files, so they generate the same uninstaller. A failed copy aborts the
; compile rather than leave the uninstaller unsigned.
;
; cpack passes the paths with forward slashes, which cmd's copy does not accept for its
; source, so they are converted first.
!ifdef ORCA_EXPORT_UNINSTALLER
!searchreplace ORCA_EXPORT_UNINSTALLER_PATH "${ORCA_EXPORT_UNINSTALLER}" "/" "\"
!uninstfinalize 'cmd /c copy /y "%1" "${ORCA_EXPORT_UNINSTALLER_PATH}"' = 0
!endif
!ifdef ORCA_SIGNED_UNINSTALLER
!searchreplace ORCA_SIGNED_UNINSTALLER_PATH "${ORCA_SIGNED_UNINSTALLER}" "/" "\"
!uninstfinalize 'cmd /c copy /y "${ORCA_SIGNED_UNINSTALLER_PATH}" "%1"' = 0
!endif