Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
947bb8e020 | ||
|
|
a9432324a7 | ||
|
|
cc274f39a8 | ||
|
|
44e9bd2916 |
@@ -682,6 +682,7 @@ jobs:
|
|||||||
run: >-
|
run: >-
|
||||||
powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass
|
powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass
|
||||||
-File scripts\windows-verify-signatures.ps1
|
-File scripts\windows-verify-signatures.ps1
|
||||||
|
app\src-tauri\target\release\triple-c.exe
|
||||||
app\src-tauri\target\release\bundle\msi\*.msi
|
app\src-tauri\target\release\bundle\msi\*.msi
|
||||||
app\src-tauri\target\release\bundle\nsis\*.exe
|
app\src-tauri\target\release\bundle\nsis\*.exe
|
||||||
|
|
||||||
|
|||||||
@@ -857,20 +857,14 @@ merge, dispatch `build-app.yml` on the branch. Every publishing step there is ga
|
|||||||
- `scripts/windows-sign.ps1` is the sign command: `signtool sign /dlib` with SHA-256 and the
|
- `scripts/windows-sign.ps1` is the sign command: `signtool sign /dlib` with SHA-256 and the
|
||||||
Microsoft timestamp server, retried. Credentials never reach a command line — the dlib reads
|
Microsoft timestamp server, retried. Credentials never reach a command line — the dlib reads
|
||||||
`AZURE_TENANT_ID` / `AZURE_CLIENT_ID` / `AZURE_CLIENT_SECRET` from the environment.
|
`AZURE_TENANT_ID` / `AZURE_CLIENT_ID` / `AZURE_CLIENT_SECRET` from the environment.
|
||||||
**It signs only an allowlist of what ships**: 5 signatures per release (the app binary twice,
|
**It signs only an allowlist of what ships**, about 4 signatures per release. Tauri also
|
||||||
because Tauri re-patches it between the MSI and NSIS bundles; the MSI; the NSIS installer;
|
presents build-time tools: the WiX extension DLLs, the NSIS plugins, and the app binary a
|
||||||
and its uninstaller). Tauri also presents build-time tools, the WiX extension DLLs and NSIS
|
second time for the second bundle type. Signing those would roughly triple the metered count
|
||||||
plugins, and signing those would more than double the metered count for no user-visible
|
for no user-visible benefit. If the app ever ships resource DLLs or sidecars, extend the
|
||||||
benefit. If the app ever ships resource DLLs or sidecars, extend the
|
|
||||||
allowlist, or they will go out unsigned. Tauri reports a failed sign command only as
|
allowlist, or they will go out unsigned. Tauri reports a failed sign command only as
|
||||||
"failed to run powershell", so the script keeps a transcript (`.code-signing/sign-output.log`,
|
"failed to run powershell", so the script keeps a transcript (`.code-signing/sign-output.log`,
|
||||||
`signtool /debug` included), and the job prints it on failure.
|
`signtool /debug` included), and the job prints it on failure.
|
||||||
- `scripts/windows-verify-signatures.ps1` checks `signtool verify /pa` plus a timestamp on the
|
- `scripts/windows-verify-signatures.ps1` checks `signtool verify /pa` plus a timestamp.
|
||||||
installers, and on the binaries **inside** the MSI (an administrative `msiexec /a` extract).
|
|
||||||
It deliberately does not check `target\release\triple-c.exe`: Tauri patches that file again
|
|
||||||
after packaging, so the loose copy is unsigned by design and is not what ships. For the NSIS
|
|
||||||
installer, which cannot be unpacked that way, it requires the signing log to show the app
|
|
||||||
binary and the uninstaller were signed.
|
|
||||||
|
|
||||||
Secrets (repository): the three `AZURE_*` above plus `ARTIFACT_SIGNING_ENDPOINT`,
|
Secrets (repository): the three `AZURE_*` above plus `ARTIFACT_SIGNING_ENDPOINT`,
|
||||||
`ARTIFACT_SIGNING_ACCOUNT_NAME`, `ARTIFACT_SIGNING_PROFILE_NAME`. They are referenced only by the
|
`ARTIFACT_SIGNING_ACCOUNT_NAME`, `ARTIFACT_SIGNING_PROFILE_NAME`. They are referenced only by the
|
||||||
|
|||||||
@@ -1,19 +1,11 @@
|
|||||||
# windows-verify-signatures.ps1 <path-or-wildcard>... - fail unless everything
|
# windows-verify-signatures.ps1 <path-or-wildcard>... - fail unless every file
|
||||||
# that ships carries a valid, timestamped Authenticode signature.
|
# carries a valid, timestamped Authenticode signature.
|
||||||
#
|
#
|
||||||
# The check that makes signing load-bearing rather than hopeful: Tauri skips
|
# The check that makes signing load-bearing rather than hopeful: Tauri skips
|
||||||
# signing silently in some configurations (no sign command, --no-sign), and an
|
# signing silently in some configurations (no sign command, --no-sign), and an
|
||||||
# unsigned installer looks exactly like a signed one until SmartScreen blocks
|
# unsigned installer looks exactly like a signed one until SmartScreen blocks
|
||||||
# it on a user's machine. Every pattern must match at least one file, so a
|
# it on a user's machine. Every pattern must match at least one file, so a
|
||||||
# bundle that was never produced cannot pass either.
|
# bundle that was never produced cannot pass either.
|
||||||
#
|
|
||||||
# Pass the installers, not target\release\triple-c.exe. The app binary users
|
|
||||||
# get is the copy inside each installer: Tauri patches the loose file with
|
|
||||||
# bundle-type information before each bundle, signs it, packages it, and
|
|
||||||
# patches it again, so the loose copy ends up unsigned by design. The MSI is
|
|
||||||
# unpacked with an administrative install and its binaries checked directly;
|
|
||||||
# the NSIS installer cannot be unpacked that way, so for it the signing log
|
|
||||||
# must show the app binary and the uninstaller were signed.
|
|
||||||
|
|
||||||
param([Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)][string[]]$Patterns)
|
param([Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)][string[]]$Patterns)
|
||||||
|
|
||||||
@@ -26,82 +18,49 @@ $files = foreach ($pattern in $Patterns) {
|
|||||||
$found
|
$found
|
||||||
}
|
}
|
||||||
|
|
||||||
function Test-Signature([IO.FileInfo]$File, [string]$Label) {
|
$failed = @()
|
||||||
# signtool's own check - chain to a trusted root under the default
|
foreach ($file in $files) {
|
||||||
# Authenticode policy - with Stop relaxed for the native call, as in
|
# signtool's own check: chain to a trusted root under the default
|
||||||
# windows-sign.ps1.
|
# Authenticode policy.
|
||||||
|
# Stop relaxed for the native call, as in windows-sign.ps1.
|
||||||
$ErrorActionPreference = 'Continue'
|
$ErrorActionPreference = 'Continue'
|
||||||
$output = & $env:TRIPLE_C_SIGNTOOL verify /pa $File.FullName 2>&1 | ForEach-Object { "$_" }
|
$verifyOutput = & $env:TRIPLE_C_SIGNTOOL verify /pa $file.FullName 2>&1 | ForEach-Object { "$_" }
|
||||||
$signtoolOk = ($LASTEXITCODE -eq 0)
|
$signtoolOk = ($LASTEXITCODE -eq 0)
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
if (-not $signtoolOk) { $output | Write-Host }
|
if (-not $signtoolOk) { $verifyOutput | Write-Host }
|
||||||
|
|
||||||
# And the timestamp, which signtool verify does not require.
|
# And the timestamp, which signtool verify does not require.
|
||||||
$sig = Get-AuthenticodeSignature -FilePath $File.FullName
|
$sig = Get-AuthenticodeSignature -FilePath $file.FullName
|
||||||
$timestamped = $null -ne $sig.TimeStamperCertificate
|
$timestamped = $null -ne $sig.TimeStamperCertificate
|
||||||
|
|
||||||
if ($signtoolOk -and $sig.Status -eq 'Valid' -and $timestamped) {
|
if ($signtoolOk -and $sig.Status -eq 'Valid' -and $timestamped) {
|
||||||
Write-Host "OK $Label - $($sig.SignerCertificate.Subject)"
|
Write-Host "OK $($file.Name) - $($sig.SignerCertificate.Subject)"
|
||||||
return $true
|
|
||||||
}
|
|
||||||
Write-Host "FAIL $Label - status $($sig.Status), signtool $(if ($signtoolOk) {'ok'} else {'failed'}), timestamped $timestamped"
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-SignedLog {
|
|
||||||
if ($env:TRIPLE_C_SIGN_LOG -and (Test-Path $env:TRIPLE_C_SIGN_LOG)) { return @(Get-Content $env:TRIPLE_C_SIGN_LOG) }
|
|
||||||
return @()
|
|
||||||
}
|
|
||||||
|
|
||||||
$failed = @()
|
|
||||||
$nsisBuilt = $false
|
|
||||||
foreach ($file in $files) {
|
|
||||||
if (-not (Test-Signature $file $file.Name)) { $failed += $file.Name }
|
|
||||||
if ($file.FullName -match '\\bundle\\nsis\\') { $nsisBuilt = $true }
|
|
||||||
|
|
||||||
if ($file.Extension -eq '.msi') {
|
|
||||||
$extract = Join-Path ([IO.Path]::GetTempPath()) ('msi-verify-' + [guid]::NewGuid().ToString('N'))
|
|
||||||
$proc = Start-Process msiexec.exe -Wait -PassThru `
|
|
||||||
-ArgumentList '/a', "`"$($file.FullName)`"", '/qn', "TARGETDIR=`"$extract`""
|
|
||||||
$inner = @()
|
|
||||||
if ($proc.ExitCode -eq 0) { $inner = @(Get-ChildItem -Path $extract -Recurse -File -Include *.exe, *.dll) }
|
|
||||||
if ($proc.ExitCode -ne 0) {
|
|
||||||
Write-Host "FAIL $($file.Name) - administrative extract exited $($proc.ExitCode)"
|
|
||||||
$failed += "$($file.Name) (extract)"
|
|
||||||
} elseif ($inner.Count -eq 0) {
|
|
||||||
Write-Host "FAIL $($file.Name) - contains no executable to check"
|
|
||||||
$failed += "$($file.Name) (no executable)"
|
|
||||||
}
|
|
||||||
foreach ($f in $inner) {
|
|
||||||
if (-not (Test-Signature $f "$($file.Name) > $($f.Name)")) { $failed += "$($file.Name) > $($f.Name)" }
|
|
||||||
}
|
|
||||||
Remove-Item -Recurse -Force $extract -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# What the NSIS installer carries but cannot be unpacked here. windows-sign.ps1
|
|
||||||
# logs every file it signs. The app binary is signed in place under
|
|
||||||
# target\release; the uninstaller is the file makensis wrote under the job's
|
|
||||||
# temp directory (see windows-signing-setup.ps1) - and makensis ignores the
|
|
||||||
# sign command's exit code for it, so without this a failure there is silent.
|
|
||||||
if ($nsisBuilt) {
|
|
||||||
$log = Get-SignedLog
|
|
||||||
$appSigned = @($log | Where-Object { $_ -match '\\target\\release\\[^\\]+\.exe$' })
|
|
||||||
if ($appSigned.Count -eq 0) {
|
|
||||||
Write-Host 'FAIL app binary - no signature was logged for it before packaging'
|
|
||||||
$failed += 'app binary'
|
|
||||||
} else {
|
} else {
|
||||||
Write-Host "OK app binary - signed before packaging ($($appSigned.Count)x)"
|
Write-Host "FAIL $($file.Name) - status $($sig.Status), signtool $(if ($signtoolOk) {'ok'} else {'failed'}), timestamped $timestamped"
|
||||||
|
$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
|
$tmp = $env:TRIPLE_C_SIGN_TMP
|
||||||
$uninstaller = @()
|
$log = $env:TRIPLE_C_SIGN_LOG
|
||||||
if ($tmp) { $uninstaller = @($log | Where-Object { $_.StartsWith($tmp, [StringComparison]::OrdinalIgnoreCase) }) }
|
$signedInTmp = @()
|
||||||
if ($uninstaller.Count -eq 0) {
|
if ($tmp -and $log -and (Test-Path $log)) {
|
||||||
Write-Host 'FAIL NSIS uninstaller - no signature was logged for it'
|
$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'
|
$failed += 'NSIS uninstaller'
|
||||||
} else {
|
} else {
|
||||||
Write-Host "OK NSIS uninstaller - signed as $($uninstaller[-1])"
|
Write-Host "OK NSIS uninstaller - signed as $($signedInTmp[-1])"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($failed.Count -gt 0) { throw "Not validly signed: $($failed -join ', ')" }
|
if ($failed.Count -gt 0) { throw "Not validly signed: $($failed -join ', ')" }
|
||||||
Write-Host 'Everything that ships is signed and timestamped.'
|
Write-Host "All $(@($files).Count) files are signed and timestamped."
|
||||||
|
|||||||
Reference in New Issue
Block a user