[packaging] (Windows) Added script to sign binaries

This commit is contained in:
Alexandr Stelnykovych 2025-03-06 14:50:00 +02:00
parent 9324c59f7b
commit 911255e1d2
2 changed files with 224 additions and 4 deletions

View file

@ -1,16 +1,57 @@
# Tested with docker image 'abrarov/msvc-2022:latest'
# sha256:f49435d194108cd56f173ad5bc6a27c70eed98b7e8cd54488f5acd85efbd51c9
#------------------------------------------------------------------------------
# Portmaster Windows Installer Generator
#------------------------------------------------------------------------------
# This script creates Windows installers (MSI and NSIS) for Portmaster application
# by combining pre-compiled binaries and packaging them with Tauri.
#
# ## Workflow for creating Portmaster Windows installers:
#
# 1. Compile Core Binaries (Linux environment)
# ```
# earthly +release-prep
# ```
# This compiles and places files into the 'dist' folder with the required structure.
# Note: Latest KEXT binaries and Intel data will be downloaded from https://updates.safing.io
#
# 2. Compile Windows-Specific Binaries (Windows environment)
# Some files cannot be compiled by Earthly and require Windows.
# - Compile 'portmaster-core.dll' from the /windows_core_dll folder
# - Copy the compiled DLL to <project-root>/dist/download/windows_amd64
#
# 3. Sign All Binaries (Windows environment)
# ```
# .\sign_binaries_in_dist.ps1 -certSha1 <SHA1_of_the_certificate>
# ```
# This signs all binary files in the dist directory
#
# 4. Create Installers (Windows environment)
# Note! You can run it from docker container (see example bellow).
# ```
# .\generate_windows_installers.ps1
# ```
# Installers will be placed in <project-root>/dist/windows_amd64
#
# 5. Sign Installers (Windows environment)
# ```
# .\sign_binaries_in_dist.ps1 -certSha1 <SHA1_of_the_certificate>
# ```
# This signs the newly created installer files
#
#------------------------------------------------------------------------------
# Running inside Docker container
# Tested with docker image 'abrarov/msvc-2022:latest'
# sha256:f49435d194108cd56f173ad5bc6a27c70eed98b7e8cd54488f5acd85efbd51c9
#
# Note! Ensure you switched Docker Desktop to use Windows containers.
# Start powershell and cd to the root of the project.
# Then run:
# $path = Convert-Path . # Get the absolute path of the current directory
# docker run -it --rm -v "${path}:C:/app" -w "C:/app" abrarov/msvc-2022 powershell -NoProfile -File C:/app/packaging/windows/generate_windows_installers.ps1
#------------------------------------------------------------------------------
#
# Optional arguments:
# -i, --interactive: Can prompt for user input (e.g. when a file is not found in the primary folder but found in the alternate folder)
#------------------------------------------------------------------------------
param (
[Alias('i')]
[switch]$interactive

View file

@ -0,0 +1,179 @@
param (
[Parameter(Mandatory=$false)]
[string]$certSha1,
[Parameter(Mandatory=$false)]
[string]$timestampServer = "http://timestamp.digicert.com"
)
function Show-Help {
Write-Host "Usage: sign_binaries_in_dist.ps1 -certSha1 <CERT_SHA1> [-timestampServer <TIMESTAMP_SERVER>]"
Write-Host ""
Write-Host "This script signs all binary files located under the '<project root>\dist\' directory recursively."
Write-Host "Which should be done before creating the Portmaster installer."
Write-Host ""
Write-Host "Arguments:"
Write-Host " -certSha1 The SHA1 hash of the certificate to use for signing (code signing certificate)."
Write-Host " -timestampServer The timestamp server URL to use (optional). Default is http://timestamp.digicert.com."
Write-Host ""
Write-Host "Example:"
Write-Host " .\sign_binaries_in_dist.ps1 -certSha1 ABCDEF1234567890ABCDEF1234567890ABCDEF12"
}
# Show help if no certificate SHA1 provided or help flag used
if (-not $certSha1 -or ($args -contains "-h") -or ($args -contains "-help") -or ($args -contains "/h")) {
Show-Help
exit 0
}
# Find signtool.exe - simplified approach
function Find-SignTool {
# First try the PATH
$signtool = Get-Command signtool.exe -ErrorAction SilentlyContinue
if ($signtool) { return $signtool }
Write-Host "[+] signtool.exe not found in PATH. Searching in common locations..."
# Common locations for signtool
$commonLocations = @(
# Windows SDK paths
"${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe",
"${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x86\signtool.exe",
# Visual Studio paths via vswhere
(& "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.Component.MSBuild -find "**/signtool.exe" -ErrorAction SilentlyContinue)
)
foreach ($location in $commonLocations) {
$tools = Get-ChildItem -Path $location -ErrorAction SilentlyContinue |
Sort-Object -Property FullName -Descending
if ($tools -and $tools.Count -gt 0) {
return $tools[0] # Return the first match
}
}
return $null
}
function Get-SignatureInfo {
param(
[string]$filePath
)
# Get the raw output from signtool
$rawOutput = & $signtool verify /pa /v $filePath 2>&1
# Filter output to exclude everything after the timestamp line
$filteredOutput = @()
foreach ($line in $rawOutput) {
if ($line -match "The signature is timestamped:") {
break
}
$filteredOutput += $line
}
# Extract last subject in the signing chain - it's typically the last "Issued to:" entry
$lastSubject = ($filteredOutput | Select-String -Pattern "Issued to: (.*)$" | Select-Object -Last 1 | ForEach-Object { $_.Matches.Groups[1].Value })
# Create signature info object
$signInfo = @{
"IsSigned" = $LASTEXITCODE -eq 0
"Subject" = ($filteredOutput | Select-String -Pattern "Issued to: (.*)$" | ForEach-Object { $_.Matches.Groups[1].Value }) -join ", "
"Issuer" = ($filteredOutput | Select-String -Pattern "Issued by: (.*)$" | ForEach-Object { $_.Matches.Groups[1].Value }) -join ", "
"ExpirationDate" = ($filteredOutput | Select-String -Pattern "Expires: (.*)$" | ForEach-Object { $_.Matches.Groups[1].Value }) -join ", "
"SubjectLast" = $lastSubject
"SignedBySameCert" = $false
}
# Check if signed by our certificate
$null = & $signtool verify /pa /sha1 $certSha1 $filePath 2>&1
$signInfo.SignedBySameCert = $LASTEXITCODE -eq 0
return $signInfo
}
# Find dist directory relative to script location
$distDir = Join-Path $PSScriptRoot "../../dist"
if (-not (Test-Path -Path $distDir)) {
Write-Host "The directory '$distDir' does not exist." -ForegroundColor Red
exit 1
}
$distDir = Resolve-Path (Join-Path $PSScriptRoot "../../dist") # normalize path
# Find signtool.exe
$signtool = Find-SignTool
if (-not $signtool) {
Write-Host "signtool.exe not found in any standard location." -ForegroundColor Red
Write-Host "Please install one of the following:" -ForegroundColor Yellow
Write-Host "- Windows SDK" -ForegroundColor Yellow
Write-Host "- Visual Studio with the 'Desktop development with C++' workload" -ForegroundColor Yellow
Write-Host "- Visual Studio Build Tools with the 'Desktop development with C++' workload" -ForegroundColor Yellow
exit 1
}
Write-Host "[i] Using signtool: $($signtool)"
# Sign all binary files in the dist directory
try {
# Define extensions for files that should be signed
$binaryExtensions = @('.exe', '.dll', '.sys', '.msi')
# Get all files with binary extensions
$files = Get-ChildItem -Path $distDir -Recurse -File | Where-Object {
$extension = [System.IO.Path]::GetExtension($_.Name).ToLower()
$binaryExtensions -contains $extension
}
$totalFiles = $files.Count
$signedFiles = 0
$alreadySignedFiles = 0
$wrongCertFiles = 0
$filesToSign = @()
Write-Host "[+] Found $totalFiles binary files to process" -ForegroundColor Green
foreach ($file in $files) {
$relativeFileName = $file.FullName.Replace("$distDir\", "")
# Get signature information
$signInfo = Get-SignatureInfo -filePath $file.FullName
if ($signInfo.IsSigned) {
if ($signInfo.SignedBySameCert) {
Write-Host -NoNewline " [signed OK ]" -ForegroundColor Green
Write-Host -NoNewline " $($relativeFileName)" -ForegroundColor Blue
Write-Host "`t: signed by our certificate"
$alreadySignedFiles++
} else {
Write-Host -NoNewline " [different ]" -ForegroundColor Yellow
Write-Host -NoNewline " $($relativeFileName)" -ForegroundColor Blue
Write-Host "`t: signed by different certificate [$($signInfo.SubjectLast)]"
$wrongCertFiles++
}
} else {
Write-Host -NoNewline " [NOT signed]" -ForegroundColor Red
Write-Host -NoNewline " $($relativeFileName)" -ForegroundColor Blue
Write-Host "`t: not signed"
$filesToSign += $file.FullName
}
}
# Batch sign files
if ($filesToSign.Count -gt 0) {
Write-Host "`n[+] Signing $($filesToSign.Count) files in batch..." -ForegroundColor Green
& $signtool sign /tr $timestampServer /td sha256 /fd sha256 /sha1 $certSha1 /v $filesToSign
if ($LASTEXITCODE -ne 0) {
Write-Host "Failed to sign files!" -ForegroundColor Red
exit 1
}
$signedFiles = $filesToSign.Count
} else {
Write-Host "`n[+] No files need signing." -ForegroundColor Green
}
Write-Host "`n[+] Summary:" -ForegroundColor Green
Write-Host " - Total binary files found: $totalFiles"
Write-Host " - Files already signed with our certificate: $alreadySignedFiles"
Write-Host " - Files signed with different certificate: $wrongCertFiles"
Write-Host " - Files newly signed: $signedFiles"
} catch {
Write-Host "An error occurred: $_" -ForegroundColor Red
exit 1
}