fix(ci): pass the sign command with --config and prove the uninstaller was signed
Secret Scan / scan (push) Successful in 8s
Build App (Preview) / compute-version (pull_request) Successful in 12s
Secret Scan / scan (pull_request) Successful in 5s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m48s
Build App (Preview) / test (pull_request) Successful in 3m41s
Build App (Preview) / build-linux (pull_request) Successful in 7m8s
Build App (Preview) / build-windows (pull_request) Failing after 8m53s
Build App (Preview) / prune-previews (pull_request) Skipped
Secret Scan / scan (push) Successful in 8s
Build App (Preview) / compute-version (pull_request) Successful in 12s
Secret Scan / scan (pull_request) Successful in 5s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m48s
Build App (Preview) / test (pull_request) Successful in 3m41s
Build App (Preview) / build-linux (pull_request) Successful in 7m8s
Build App (Preview) / build-windows (pull_request) Failing after 8m53s
Build App (Preview) / prune-previews (pull_request) Skipped
The first signing run built nothing signed, and "Verify signatures" failed it as intended. The Tauri 2 CLI never reads TAURI_CONFIG. It only sets that variable for tauri-build, so the sign command was dropped silently, just as the inline beforeBuildCommand override had been for as long as the Windows jobs have set it. The setup now writes a config file, and the build passes it with `cargo tauri build --config`. Review follow-ups: - The NSIS uninstaller is written to %TEMP% and signed from 32-bit makensis. SYSTEM's %TEMP% sits under System32, which WOW64 redirects for makensis but not for the x64 signtool, so they would disagree about where the file is. %TEMP% and %TMP% now point into the workspace. makensis ignores the sign command's exit code for the uninstaller, so windows-sign.ps1 logs every file it signs, and the verify step requires a logged signature under that temp directory. - The signing client is pinned by SHA-512, so the pin can be checked against nuget.org's published packageHash. - tauri-cli on Windows is pinned to =2.11.0 --locked, the @tauri-apps/cli version the Linux and macOS jobs run from the lockfile, instead of "^2". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,7 +47,14 @@ $arguments = @(
|
||||
$ErrorActionPreference = 'Continue'
|
||||
for ($attempt = 1; $attempt -le 3; $attempt++) {
|
||||
& $env:TRIPLE_C_SIGNTOOL @arguments 2>&1 | ForEach-Object { "$_" }
|
||||
if ($LASTEXITCODE -eq 0) { exit 0 }
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
# The evidence "Verify signatures" needs for files it cannot see
|
||||
# afterwards - the NSIS uninstaller is embedded in the installer.
|
||||
if ($env:TRIPLE_C_SIGN_LOG) {
|
||||
[IO.File]::AppendAllText($env:TRIPLE_C_SIGN_LOG, "$Path`n", (New-Object System.Text.UTF8Encoding $false))
|
||||
}
|
||||
exit 0
|
||||
}
|
||||
Write-Host "signtool exited $LASTEXITCODE signing $Path (attempt $attempt of 3)"
|
||||
if ($attempt -lt 3) { Start-Sleep -Seconds (10 * $attempt) }
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
#
|
||||
# Run once per job, before `cargo tauri build`. It fetches the two things the
|
||||
# build VM does not carry, checks each against a pinned hash, and hands the
|
||||
# rest of the job what `windows-sign.ps1` needs through $GITHUB_ENV:
|
||||
# rest of the job what `windows-sign.ps1` needs through $GITHUB_ENV, including
|
||||
# TRIPLE_C_TAURI_SIGN_CONFIG - a config file for `cargo tauri build --config`.
|
||||
# Not the TAURI_CONFIG variable: the v2 CLI never reads it (it only *sets* it,
|
||||
# for tauri-build), so a sign command put there is silently ignored.
|
||||
#
|
||||
# * Microsoft.ArtifactSigning.Client - the signtool "dlib" that forwards the
|
||||
# digest to Azure instead of signing with a local certificate.
|
||||
@@ -19,8 +22,10 @@
|
||||
# SysWOW64. The workspace sits under systemprofile\.cache, which the VM
|
||||
# junctions so both views resolve (see "Build Tauri app" in build-app.yml).
|
||||
#
|
||||
# Bumping a pin: take the new version's hash from nuget.org / the .NET
|
||||
# release metadata (releases.json), never from a download you just made.
|
||||
# Bumping a pin: take the new version's hash from the publisher, never from a
|
||||
# download you just made - the client's SHA-512 is the base64 `packageHash` in
|
||||
# its nuget.org catalog entry (hex here), the runtime's is in .NET's
|
||||
# releases.json.
|
||||
#
|
||||
# Required environment (repository secrets): AZURE_TENANT_ID, AZURE_CLIENT_ID,
|
||||
# AZURE_CLIENT_SECRET, ARTIFACT_SIGNING_ENDPOINT, ARTIFACT_SIGNING_ACCOUNT_NAME,
|
||||
@@ -32,7 +37,7 @@ $ProgressPreference = 'SilentlyContinue'
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
$ClientVersion = '1.0.128'
|
||||
$ClientSha256 = '74bd7d27e6ce1051409c38d9b46bc8df0400ecd643d51ffbf2ac00869061e40b'
|
||||
$ClientSha512 = '98f06a691f4fc2fa22f19dcf8556733e98607fbef91a312c453b9b0798cc9088dae0acb36e389b552a11b4d2320324785b8541c2b51091a724c05bc5df5cbf95'
|
||||
$ClientUrl = "https://api.nuget.org/v3-flatcontainer/microsoft.artifactsigning.client/$ClientVersion/microsoft.artifactsigning.client.$ClientVersion.nupkg"
|
||||
|
||||
$DotnetVersion = '10.0.12'
|
||||
@@ -69,7 +74,7 @@ function Get-Verified([string]$Url, [string]$Name, [string]$Algorithm, [string]$
|
||||
|
||||
# The signing client. A .nupkg is a zip; extract it with the framework rather
|
||||
# than Expand-Archive, which on PowerShell 5.1 refuses any extension but .zip.
|
||||
$nupkg = Get-Verified $ClientUrl 'client.nupkg' 'SHA256' $ClientSha256
|
||||
$nupkg = Get-Verified $ClientUrl 'client.nupkg' 'SHA512' $ClientSha512
|
||||
$clientDir = Join-Path $root 'client'
|
||||
[IO.Compression.ZipFile]::ExtractToDirectory($nupkg, $clientDir)
|
||||
$dlib = Join-Path $clientDir 'bin\x64\Azure.CodeSigning.Dlib.dll'
|
||||
@@ -116,7 +121,8 @@ $utf8 = New-Object System.Text.UTF8Encoding $false
|
||||
|
||||
# Tauri runs this for every file it signs - the app binary, the MSI, the NSIS
|
||||
# installer and (from inside makensis) the uninstaller - with %1 replaced by
|
||||
# the path. Object form, so paths with spaces survive.
|
||||
# the path. Object form, so paths with spaces survive. beforeBuildCommand is
|
||||
# blanked because the job builds the frontend in its own step.
|
||||
$signScript = Join-Path $workspace 'scripts\windows-sign.ps1'
|
||||
$tauriConfig = @{
|
||||
build = @{ beforeBuildCommand = '' }
|
||||
@@ -124,7 +130,21 @@ $tauriConfig = @{
|
||||
cmd = 'powershell'
|
||||
args = @('-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', $signScript, '%1')
|
||||
} } }
|
||||
} | ConvertTo-Json -Depth 8 -Compress
|
||||
} | ConvertTo-Json -Depth 8
|
||||
$tauriConfigPath = Join-Path $root 'tauri.signing.conf.json'
|
||||
[IO.File]::WriteAllText($tauriConfigPath, $tauriConfig, $utf8)
|
||||
|
||||
# The job's temp directory moves into the workspace too. makensis writes the
|
||||
# uninstaller to %TEMP% before signing it, and SYSTEM's own %TEMP% is under
|
||||
# System32: the 32-bit makensis and PowerShell would see the SysWOW64 copy of
|
||||
# that path while the x64 signtool opens the real one, and fail to find the
|
||||
# file. makensis ignores the sign command's exit code for the uninstaller
|
||||
# (Tauri emits `!uninstfinalize` without a compare), so that failure would be
|
||||
# silent - hence the signing log, which "Verify signatures" reads to require
|
||||
# that a file under this directory, i.e. the uninstaller, really was signed.
|
||||
$tmpDir = Join-Path $root 'tmp'
|
||||
New-Item -ItemType Directory -Path $tmpDir | Out-Null
|
||||
$signLog = Join-Path $root 'signed.log'
|
||||
|
||||
# $GITHUB_ENV is KEY=VALUE lines. Written without a BOM: PowerShell 5.1's
|
||||
# utf8 encoding adds one, which would corrupt the first key.
|
||||
@@ -135,7 +155,11 @@ $lines = @(
|
||||
"TRIPLE_C_SIGN_TIMESTAMP=$TimestampUrl"
|
||||
"DOTNET_ROOT=$dotnetDir"
|
||||
"DOTNET_ROOT_X64=$dotnetDir"
|
||||
"TAURI_CONFIG=$tauriConfig"
|
||||
"TRIPLE_C_TAURI_SIGN_CONFIG=$tauriConfigPath"
|
||||
"TRIPLE_C_SIGN_LOG=$signLog"
|
||||
"TRIPLE_C_SIGN_TMP=$tmpDir"
|
||||
"TEMP=$tmpDir"
|
||||
"TMP=$tmpDir"
|
||||
)
|
||||
[IO.File]::AppendAllText($env:GITHUB_ENV, (($lines -join "`n") + "`n"), $utf8)
|
||||
Write-Host 'Code signing prepared.'
|
||||
|
||||
@@ -40,5 +40,27 @@ foreach ($file in $files) {
|
||||
$failed += $file.Name
|
||||
}
|
||||
}
|
||||
|
||||
# The NSIS uninstaller is signed from inside makensis, which ignores the sign
|
||||
# command's exit code, and it ends up embedded in the installer where the
|
||||
# checks above cannot reach it. windows-sign.ps1 logs every file it signs; the
|
||||
# uninstaller is the one makensis wrote under the job's temp directory (see
|
||||
# windows-signing-setup.ps1), so require at least one logged path there.
|
||||
$nsisBuilt = @($files | Where-Object { $_.FullName -match '\\bundle\\nsis\\' }).Count -gt 0
|
||||
if ($nsisBuilt) {
|
||||
$tmp = $env:TRIPLE_C_SIGN_TMP
|
||||
$log = $env:TRIPLE_C_SIGN_LOG
|
||||
$signedInTmp = @()
|
||||
if ($tmp -and $log -and (Test-Path $log)) {
|
||||
$signedInTmp = @(Get-Content $log | Where-Object { $_.StartsWith($tmp, [StringComparison]::OrdinalIgnoreCase) })
|
||||
}
|
||||
if ($signedInTmp.Count -eq 0) {
|
||||
Write-Host 'FAIL NSIS uninstaller - no successful signature was logged for it'
|
||||
$failed += 'NSIS uninstaller'
|
||||
} else {
|
||||
Write-Host "OK NSIS uninstaller - signed as $($signedInTmp[-1])"
|
||||
}
|
||||
}
|
||||
|
||||
if ($failed.Count -gt 0) { throw "Not validly signed: $($failed -join ', ')" }
|
||||
Write-Host "All $(@($files).Count) files are signed and timestamped."
|
||||
|
||||
Reference in New Issue
Block a user