Compare commits

..

4 Commits

Author SHA1 Message Date
SoftFever
a913e2bcd8 ci(windows): detect untrusted root via structured signature status
The previous text match on signtool output was flaky: signtool interleaves
stdout (chain details) and stderr (error), so the wrapped "untrusted root"
phrase stayed contiguous for one file but was split for another, causing the
verify step to pass orca-slicer.exe but throw on OrcaSlicer.dll.

Use Get-AuthenticodeSignature instead, whose StatusMessage is a single clean
string, to decide the untrusted-root exception. signtool remains the strict
primary check; this only governs the -AllowUntrustedRoot test-cert path.
2026-05-30 09:59:55 +08:00
SoftFever
ad737d1080 ci(windows): reset exit code after accepting untrusted test-cert root
The verify script accepted both signed binaries and printed "verification
passed", but the step still failed: signtool exits 1 on the untrusted test
root, and GitHub's pwsh wrapper exits with that lingering $LASTEXITCODE.
Add an explicit `exit 0` on the success path so the step passes.
2026-05-30 09:57:41 +08:00
SoftFever
48acf4f13b ci(windows): tolerate untrusted root in signature verification for test cert
signtool verify /pa exits non-zero when the certificate chain terminates in
an untrusted root, which is always the case for the SignPath self-signed test
certificate. Add an -AllowUntrustedRoot switch to verify-authenticode.ps1 that
accepts a signed-but-untrusted-root result (while still failing on unsigned or
otherwise invalid files), and pass it from the workflow during test-signing.

Remove the switch once signing-policy-slug moves to release-signing with a
production CA-issued certificate, so release builds enforce a fully trusted chain.
2026-05-30 01:52:51 +08:00
SoftFever
ebdfa74ce8 ci(windows): wire SignPath test-signing for portable bundle
Add SignPath artifact configuration (windows-portable-v1) signing only the
first-party binaries orca-slicer.exe and OrcaSlicer.dll, plus verification
and inventory scripts. The Windows build job uploads the unsigned portable
bundle, submits it to SignPath (test-signing), verifies the returned
signatures, and rebuilds the installer/zip from the signed binaries.

project-slug matches the SignPath project (OrcaSlicer).
2026-05-29 23:42:38 +08:00
8 changed files with 268 additions and 80 deletions

View File

@@ -13,6 +13,8 @@ on:
- 'localization/**'
- 'resources/**'
- ".github/workflows/build_*.yml"
- 'signpath/**'
- 'scripts/*.ps1'
- 'scripts/flatpak/**'
pull_request:
@@ -26,9 +28,11 @@ on:
- '**/CMakeLists.txt'
- 'version.inc'
- ".github/workflows/build_*.yml"
- 'signpath/**'
- 'build_linux.sh'
- 'build_release_vs2022.bat'
- 'build_release_macos.sh'
- 'scripts/*.ps1'
- 'scripts/flatpak/**'

View File

@@ -292,6 +292,53 @@ jobs:
# WindowsSDKVersion: '10.0.26100.0\'
run: .\build_release_vs.bat slicer
- name: Pack PDB
if: runner.os == 'Windows' && !vars.SELF_HOSTED
working-directory: ${{ github.workspace }}/build/src/Release
shell: cmd
run: '"C:/Program Files/7-Zip/7z.exe" a -m0=lzma2 -mx9 Debug_PDB_${{ env.ver }}_for_developers_only.7z *.pdb'
- 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
uses: actions/upload-artifact@v7
with:
name: OrcaSlicer_Windows_${{ env.ver }}_portable_unsigned
path: ${{ github.workspace }}/build/OrcaSlicer
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
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 }}
wait-for-completion: true
output-artifact-directory: ${{ github.workspace }}/build/signpath/windows-portable
- 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
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 }}/build/signpath/windows-portable' -AllowUntrustedRoot
- name: Replace Windows portable bundle 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
shell: pwsh
run: |
$source = Join-Path "${{ github.workspace }}" "build/signpath/windows-portable"
$destination = Join-Path "${{ github.workspace }}" "build/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
- name: Create installer Win
if: runner.os == 'Windows' && !vars.SELF_HOSTED
working-directory: ${{ github.workspace }}/build
@@ -301,14 +348,14 @@ jobs:
- name: Pack app
if: runner.os == 'Windows'
working-directory: ${{ github.workspace }}/build
shell: cmd
run: '"C:/Program Files/7-Zip/7z.exe" a -tzip OrcaSlicer_Windows_${{ env.ver }}_portable.zip ${{ github.workspace }}/build/OrcaSlicer'
- name: Pack PDB
if: runner.os == 'Windows' && !vars.SELF_HOSTED
working-directory: ${{ github.workspace }}/build/src/Release
shell: cmd
run: '"C:/Program Files/7-Zip/7z.exe" a -m0=lzma2 -mx9 Debug_PDB_${{ env.ver }}_for_developers_only.7z *.pdb'
shell: pwsh
run: |
$zipPath = "OrcaSlicer_Windows_${{ env.ver }}_portable.zip"
Remove-Item -LiteralPath $zipPath -Force -ErrorAction SilentlyContinue
& "C:/Program Files/7-Zip/7z.exe" a -tzip $zipPath "${{ github.workspace }}/build/OrcaSlicer"
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
- name: Upload artifacts Win zip
if: runner.os == 'Windows'
@@ -316,6 +363,7 @@ jobs:
with:
name: OrcaSlicer_Windows_${{ env.ver }}_portable
path: ${{ github.workspace }}/build/OrcaSlicer
if-no-files-found: error
- name: Upload artifacts Win installer
if: runner.os == 'Windows' && !vars.SELF_HOSTED

View File

@@ -0,0 +1,48 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
[string]$ArtifactDirectory
)
$ErrorActionPreference = "Stop"
function Get-RelativePath {
param(
[Parameter(Mandatory = $true)]
[string]$Root,
[Parameter(Mandatory = $true)]
[string]$Path
)
$rootWithSeparator = $Root.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
$rootUri = [Uri]$rootWithSeparator
$pathUri = [Uri]$Path
[Uri]::UnescapeDataString($rootUri.MakeRelativeUri($pathUri).ToString()).Replace("/", [IO.Path]::DirectorySeparatorChar)
}
$artifactRoot = (Resolve-Path -LiteralPath $ArtifactDirectory).Path
$binaries = Get-ChildItem -LiteralPath $artifactRoot -Recurse -File |
Where-Object { $_.Extension -in @(".exe", ".dll") } |
Sort-Object -Property FullName
if (-not $binaries) {
Write-Warning "No .exe or .dll files found under '$artifactRoot'."
exit 0
}
$binaries | ForEach-Object {
$signature = Get-AuthenticodeSignature -LiteralPath $_.FullName
[pscustomobject]@{
RelativePath = Get-RelativePath -Root $artifactRoot -Path $_.FullName
Status = $signature.Status
SignatureType = $signature.SignatureType
Signer = if ($signature.SignerCertificate) { $signature.SignerCertificate.Subject } else { "" }
SignerThumbprint = if ($signature.SignerCertificate) { $signature.SignerCertificate.Thumbprint } else { "" }
Timestamped = [bool]$signature.TimeStamperCertificate
TimeStamper = if ($signature.TimeStamperCertificate) { $signature.TimeStamperCertificate.Subject } else { "" }
}
} | Format-Table -AutoSize

View File

@@ -0,0 +1,115 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
[string]$ArtifactDirectory,
[string[]]$Files = @(
"orca-slicer.exe",
"OrcaSlicer.dll"
),
[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.
[switch]$AllowUntrustedRoot
)
$ErrorActionPreference = "Stop"
function Resolve-SignToolPath {
param(
[string]$ExplicitPath
)
if ($ExplicitPath) {
if (Test-Path -LiteralPath $ExplicitPath -PathType Leaf) {
return (Resolve-Path -LiteralPath $ExplicitPath).Path
}
throw "SignTool was not found at '$ExplicitPath'."
}
$fromPath = Get-Command -Name "signtool.exe" -ErrorAction SilentlyContinue
if ($fromPath) {
return $fromPath.Source
}
$candidateRoots = @(
"${env:ProgramFiles(x86)}\Windows Kits\10\bin",
"${env:ProgramFiles}\Windows Kits\10\bin"
) | Where-Object { $_ -and (Test-Path -LiteralPath $_ -PathType Container) }
foreach ($root in $candidateRoots) {
$candidate = Get-ChildItem -LiteralPath $root -Recurse -Filter "signtool.exe" -File -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match "\\(x64|arm64)\\signtool\.exe$" } |
Sort-Object -Property FullName -Descending |
Select-Object -First 1
if ($candidate) {
return $candidate.FullName
}
}
throw "signtool.exe was not found. Install the Windows SDK or pass -SignToolPath."
}
$artifactRoot = (Resolve-Path -LiteralPath $ArtifactDirectory).Path
$signtool = Resolve-SignToolPath -ExplicitPath $SignToolPath
Write-Host "Using SignTool: $signtool"
Write-Host "Verifying Authenticode signatures in: $artifactRoot"
foreach ($relativePath in $Files) {
$filePath = Join-Path $artifactRoot $relativePath
if (-not (Test-Path -LiteralPath $filePath -PathType Leaf)) {
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
$ErrorActionPreference = "Continue"
try {
$output = & $signtool verify /pa /all /tw /v $filePath 2>&1
$exitCode = $LASTEXITCODE
}
finally {
$ErrorActionPreference = $previousErrorActionPreference
}
$output | ForEach-Object { Write-Host $_ }
if ($exitCode -eq 0) {
continue
}
if ($AllowUntrustedRoot) {
# signtool interleaves its stdout (chain details) and stderr (the error)
# unpredictably, so the error text cannot be matched reliably. Use the
# 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)."
continue
}
}
throw "SignTool verification failed for '$relativePath' with exit code $exitCode."
}
Write-Host "Authenticode verification passed."
# signtool exits non-zero for the untrusted test-cert root even when we accept
# it above, leaving $LASTEXITCODE = 1. The GitHub Actions pwsh wrapper exits
# with that lingering code, so reset it to report success explicitly.
exit 0

16
signpath/README.md Normal file
View File

@@ -0,0 +1,16 @@
# SignPath configurations
This directory contains SignPath artifact configurations used by GitHub Actions.
## `windows-portable-v1`
`windows-portable-v1.xml` is the initial conservative Windows portable-bundle signing configuration. It signs only the two first-party binaries:
- `orca-slicer.exe`
- `OrcaSlicer.dll`
Do not broaden this to all DLLs without first confirming ownership, provenance, and whether upstream vendor signatures should be verified instead.
The Windows workflow uploads `${{ github.workspace }}/build/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 `<zip-file>` with `orca-slicer.exe` and `OrcaSlicer.dll` directly beneath it.
The release portable ZIP is a separate archive created with 7-Zip from `${{ github.workspace }}/build/OrcaSlicer`; that archive keeps the top-level `OrcaSlicer/` folder. After SignPath returns the signed artifact, the workflow copies the signed files back into `build/OrcaSlicer` and recreates the portable release ZIP so the public ZIP layout stays unchanged.

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<artifact-configuration xmlns="http://signpath.io/artifact-configuration/v1">
<zip-file>
<pe-file path="orca-slicer.exe">
<authenticode-sign />
</pe-file>
<pe-file path="OrcaSlicer.dll">
<authenticode-sign />
</pe-file>
</zip-file>
</artifact-configuration>

View File

@@ -71,7 +71,6 @@
#include <dbt.h>
#include <shlobj.h>
#include <shellapi.h>
#include <windowsx.h> // GET_X_LPARAM / GET_Y_LPARAM
#endif // _WIN32
#ifdef __WXGTK__
@@ -907,68 +906,27 @@ WXLRESULT MainFrame::MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam
return HTCAPTION;
}
if (!m_nchit_cache.valid || !m_topbar)
break;
// Allow resizing from top of the title bar
wxPoint mouse_pos = ::wxGetMousePosition();
if (m_topbar->GetScreenRect().GetBottom() >= mouse_pos.y) {
RECT borderThickness;
SetRectEmpty(&borderThickness);
AdjustWindowRectEx(&borderThickness, GetWindowLongPtr(hWnd, GWL_STYLE) & ~WS_CAPTION, FALSE, NULL);
borderThickness.left *= -1;
borderThickness.top *= -1;
wxPoint client_pos = this->ScreenToClient(mouse_pos);
// lParam already encodes the cursor screen position
// All other values come from the cache updated in WM_WINDOWPOSCHANGED/WM_DPICHANGED,
// so this handler makes zero system calls at 1000 Hz polling rates.
LONG screen_x = GET_X_LPARAM(lParam);
LONG screen_y = GET_Y_LPARAM(lParam);
bool on_top_border = client_pos.y <= borderThickness.top;
if (screen_y > m_nchit_cache.topbar_screen_bottom)
break; // below topbar: let default proc handle it
// And to allow diagonally resizing, we check if mouse is at window corner
if (client_pos.x <= borderThickness.left) {
return on_top_border ? HTTOPLEFT : HTLEFT;
} else if (client_pos.x >= GetClientSize().x - borderThickness.right) {
return on_top_border ? HTTOPRIGHT : HTRIGHT;
}
LONG client_x = screen_x - m_nchit_cache.client_origin_x;
LONG client_y = screen_y - m_nchit_cache.client_origin_y;
bool on_top_border = client_y <= m_nchit_cache.border.top;
// Check window corners for diagonal resize
if (client_x <= m_nchit_cache.border.left) {
return on_top_border ? HTTOPLEFT : HTLEFT;
} else if (client_x >= m_nchit_cache.client_width - m_nchit_cache.border.right) {
return on_top_border ? HTTOPRIGHT : HTRIGHT;
return on_top_border ? HTTOP : HTCAPTION;
}
return on_top_border ? HTTOP : HTCAPTION;
}
case WM_WINDOWPOSCHANGED: {
const auto* wp = reinterpret_cast<const WINDOWPOS*>(lParam);
if (m_topbar) {
if (!m_nchit_cache.valid) {
// First-time init (or post-DPI-change): compute border thickness and topbar height.
// WINDOWPOS x/y always hold the current window position even when SWP_NOMOVE is set.
SetRectEmpty(&m_nchit_cache.border);
AdjustWindowRectEx(&m_nchit_cache.border,
GetWindowLongPtr(hWnd, GWL_STYLE) & ~WS_CAPTION, FALSE, NULL);
m_nchit_cache.border.left *= -1;
m_nchit_cache.border.top *= -1;
m_nchit_cache.topbar_height = m_topbar->GetSize().y;
m_nchit_cache.client_width = GetClientSize().x;
m_nchit_cache.client_origin_x = wp->x + m_nchit_cache.border.left;
m_nchit_cache.client_origin_y = wp->y + 1;
m_nchit_cache.topbar_screen_bottom = m_nchit_cache.client_origin_y + m_nchit_cache.topbar_height;
m_nchit_cache.valid = true;
}
if (!(wp->flags & SWP_NOMOVE)) {
// client_origin_y = window_top + 1 (the 1-px top resize strip added in WM_NCCALCSIZE)
m_nchit_cache.client_origin_x = wp->x + m_nchit_cache.border.left;
m_nchit_cache.client_origin_y = wp->y + 1;
m_nchit_cache.topbar_screen_bottom = m_nchit_cache.client_origin_y + m_nchit_cache.topbar_height;
}
if (!(wp->flags & SWP_NOSIZE)) {
m_nchit_cache.topbar_height = m_topbar->GetSize().y;
m_nchit_cache.client_width = GetClientSize().x;
m_nchit_cache.topbar_screen_bottom = m_nchit_cache.client_origin_y + m_nchit_cache.topbar_height;
}
}
break;
}
case WM_DPICHANGED: {
// Border thickness and topbar height are DPI-dependent; force recompute.
m_nchit_cache.valid = false;
break;
}

View File

@@ -206,19 +206,6 @@ protected:
#ifdef __WXMSW__
WXLRESULT MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam) override;
// Cached values for WM_NCHITTEST to avoid 5+ system calls per mouse event at
// high polling rates (e.g. 1000 Hz mice cause visible window drag lag without this).
// Refreshed in WM_WINDOWPOSCHANGED; invalidated on DPI change.
struct {
LONG client_origin_x = 0;
LONG client_origin_y = 0; // window_top + 1 (1-px top resize strip, see WM_NCCALCSIZE)
LONG topbar_screen_bottom = 0;
int client_width = 0;
int topbar_height = 0;
RECT border = {}; // positive NC border thickness from AdjustWindowRectEx
bool valid = false;
} m_nchit_cache;
#endif
public: