[feat] add SignPath release signing and manual Windows tests

[skip ci]
This commit is contained in:
dijunkun
2026-09-09 22:57:49 +08:00
parent 211d4de241
commit 2c76caa918
7 changed files with 513 additions and 41 deletions
@@ -0,0 +1,65 @@
param(
[Parameter(Mandatory)]
[ValidateSet('PrepareBinaries', 'RestoreBinaries', 'VerifyInstaller')]
[string]$Operation,
[Parameter(Mandatory)] [string]$Artifact,
[string]$Directory,
[ValidateSet('test-signing', 'release-signing')] [string]$SigningPolicy
)
. "$PSScriptRoot/signpath-common.ps1"
if ($Operation -ne 'PrepareBinaries' -and !$SigningPolicy) {
throw 'SigningPolicy is required when verifying signed artifacts.'
}
if ($Operation -eq 'VerifyInstaller') {
Assert-CrossDeskSignature -Path $Artifact -SigningPolicy $SigningPolicy
return
}
if (!$Directory -or !(Test-Path -LiteralPath $Directory -PathType Container)) {
throw "Binaries directory does not exist: $Directory"
}
$files = @(Get-CrossDeskSigningFiles)
$temporaryDirectory = Join-Path ([IO.Path]::GetTempPath()) "crossdesk-signing-$([guid]::NewGuid())"
New-Item -ItemType Directory -Path $temporaryDirectory | Out-Null
try {
if ($Operation -eq 'PrepareBinaries') {
foreach ($name in $files) {
$source = Join-Path $Directory $name
if (!(Test-Path -LiteralPath $source -PathType Leaf)) {
throw "Missing CrossDesk binary: $source"
}
Copy-Item -LiteralPath $source -Destination (Join-Path $temporaryDirectory $name)
}
$artifactPath = [IO.Path]::GetFullPath($Artifact)
New-Item -ItemType Directory -Path (Split-Path -Parent $artifactPath) -Force | Out-Null
Compress-Archive -Path "$temporaryDirectory/*" -DestinationPath $artifactPath -Force
return
}
# Inspect the ZIP before extraction: reject missing, duplicate or unexpected
# entries (including paths) so only the four intended files can be replaced.
$archive = [IO.Compression.ZipFile]::OpenRead([IO.Path]::GetFullPath($Artifact))
try {
$entries = @($archive.Entries | ForEach-Object { $_.FullName })
$uniqueEntries = @($entries | Sort-Object -Unique)
if ($entries.Count -ne $files.Count -or $uniqueEntries.Count -ne $files.Count -or
@(Compare-Object $files $entries).Count -ne 0) {
throw "Signed archive must contain exactly: $($files -join ', ')"
}
} finally {
$archive.Dispose()
}
Expand-Archive -LiteralPath $Artifact -DestinationPath $temporaryDirectory
# Verify every file before replacing any build output.
foreach ($name in $files) {
Assert-CrossDeskSignature -Path (Join-Path $temporaryDirectory $name) -SigningPolicy $SigningPolicy
}
foreach ($name in $files) {
Copy-Item -LiteralPath (Join-Path $temporaryDirectory $name) -Destination (Join-Path $Directory $name) -Force
}
} finally {
Remove-Item -LiteralPath $temporaryDirectory -Recurse -Force
}
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<artifact-configuration xmlns="http://signpath.io/artifact-configuration/v1">
<parameters>
<parameter name="version" required="true" />
</parameters>
<zip-file>
<pe-file-set product-name="CrossDesk"
product-version="${version}"
file-version="${version}"
company-name="CrossDesk"
copyright="Copyright (C) CrossDesk contributors">
<include path="crossdesk.exe" original-filename="CrossDesk.exe" />
<include path="crossdesk_service.exe" original-filename="crossdesk_service.exe" />
<include path="crossdesk_session_helper.exe" original-filename="crossdesk_session_helper.exe" />
<include path="wgc_plugin.dll" original-filename="wgc_plugin.dll" />
<for-each>
<authenticode-sign />
</for-each>
</pe-file-set>
</zip-file>
</artifact-configuration>
@@ -0,0 +1,57 @@
$ErrorActionPreference = 'Stop'
function Get-CrossDeskSigningFiles {
# Only binaries built from this project's sources. Never include third-party DLLs.
return @('crossdesk.exe', 'crossdesk_service.exe', 'crossdesk_session_helper.exe', 'wgc_plugin.dll')
}
function Find-CrossDeskSignTool {
$command = Get-Command signtool.exe -ErrorAction SilentlyContinue
if ($command) {
return $command.Source
}
$sdk = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits/10/bin'
$tool = Get-ChildItem "$sdk/*/x64/signtool.exe" |
Sort-Object { [version]$_.Directory.Parent.Name } |
Select-Object -Last 1
if (!$tool) {
throw 'SignTool from the Windows SDK is required to verify release signatures.'
}
return $tool.FullName
}
function Assert-CrossDeskSignature {
param(
[Parameter(Mandatory)] [string]$Path,
[Parameter(Mandatory)] [ValidateSet('test-signing', 'release-signing')] [string]$SigningPolicy
)
if (!(Test-Path -LiteralPath $Path -PathType Leaf)) {
throw "Missing signed file: $Path"
}
$signature = Get-AuthenticodeSignature -LiteralPath $Path
if (!$signature.SignerCertificate) {
throw "File has no Authenticode signing certificate: $Path"
}
Write-Host "Signature: $Path | $($signature.Status) | $($signature.SignerCertificate.Subject)"
if ($SigningPolicy -eq 'test-signing') {
# Self-signed test certificates are not trusted by stock GitHub runners.
# HashMismatch, NotSigned, UnknownError, etc. must still stop the build.
if ($signature.Status -notin @('Valid', 'NotTrusted')) {
throw "Invalid test signature on ${Path}: $($signature.Status)"
}
return
}
if ($signature.Status -ne 'Valid') {
throw "Release signature is not trusted on ${Path}: $($signature.Status)"
}
if (!$signature.TimeStamperCertificate) {
throw "Release signature has no timestamp: $Path"
}
$signTool = Find-CrossDeskSignTool
& $signTool verify /pa /all /v $Path
if ($LASTEXITCODE -ne 0) {
throw "SignTool verification failed for $Path (exit $LASTEXITCODE)."
}
}