Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/scripts/sign_windows_artifact.ps1

307 lines
10 KiB
PowerShell

# Sign Windows release artifacts (EXE/DLL/MSI) with an Authenticode certificate.
#
# Certificate sources (first match wins):
# -PfxPath / $env:WIN_CODESIGN_PFX / $env:HESABIX_WIN_CODESIGN_PFX
# -Thumbprint / $env:WIN_CODESIGN_THUMBPRINT
# -SubjectContains / $env:WIN_CODESIGN_SUBJECT (certificate store lookup)
#
# Password:
# -PfxPassword / $env:WIN_CODESIGN_PASSWORD / $env:HESABIX_WIN_CODESIGN_PASSWORD
#
# Examples:
# .\scripts\sign_windows_artifact.ps1 -Path installer\windows\dist\hesabix-windows.1.2.3.msi
# $env:WIN_CODESIGN_PFX='C:\secrets\hesabix.pfx'; $env:WIN_CODESIGN_PASSWORD='...'
# .\scripts\sign_windows_artifact.ps1 -Path build\...\hesabix_ui.exe -Verify
param(
[Parameter(Mandatory = $true)]
[string[]]$Path,
[string]$PfxPath = "",
[string]$PfxPassword = "",
[string]$Thumbprint = "",
[string]$SubjectContains = "",
[string]$TimestampUrl = "",
[string]$Description = "Hesabix",
[string]$HashAlgorithm = "SHA256",
[switch]$Verify,
[switch]$DryRun,
[switch]$Help
)
$ErrorActionPreference = "Stop"
function Write-Usage {
Write-Host @"
Usage: .\scripts\sign_windows_artifact.ps1 -Path <file|folder> [-Path <more> ...] [options]
Options:
-PfxPath PATH PFX certificate file
-PfxPassword TEXT PFX password (or env WIN_CODESIGN_PASSWORD)
-Thumbprint HEX Certificate thumbprint from Windows store
-SubjectContains TEXT Find code-signing cert in store by subject substring
-TimestampUrl URL RFC3161 timestamp server (default: DigiCert)
-Description TEXT Signature description (default: Hesabix)
-HashAlgorithm ALG SHA256 (default) or SHA1
-Verify Fail if signature is missing or not Valid
-DryRun Print actions only
-Help Show help
Environment:
WIN_CODESIGN_PFX / HESABIX_WIN_CODESIGN_PFX
WIN_CODESIGN_PASSWORD / HESABIX_WIN_CODESIGN_PASSWORD
WIN_CODESIGN_THUMBPRINT
WIN_CODESIGN_SUBJECT
WIN_CODESIGN_TIMESTAMP_URL
"@
}
if ($Help) {
Write-Usage
exit 0
}
function Resolve-FirstNonEmpty {
param([string[]]$Values)
foreach ($v in $Values) {
if ($v -and $v.Trim()) { return $v.Trim() }
}
return ""
}
function Find-SignToolExe {
$cmd = Get-Command signtool.exe -ErrorAction SilentlyContinue
if ($cmd) { return $cmd.Source }
$roots = @(
"${env:ProgramFiles(x86)}\Windows Kits\10\bin",
"${env:ProgramFiles}\Windows Kits\10\bin"
)
foreach ($root in $roots) {
if (-not (Test-Path -LiteralPath $root)) { continue }
$found = Get-ChildItem -Path $root -Recurse -Filter signtool.exe -ErrorAction SilentlyContinue |
Sort-Object FullName -Descending |
Select-Object -First 1
if ($found) { return $found.FullName }
}
return $null
}
function Get-CodeSignCertificate {
param(
[string]$Pfx,
[string]$Password,
[string]$Thumb,
[string]$Subject
)
if ($Pfx) {
if (-not (Test-Path -LiteralPath $Pfx)) {
throw "PFX not found: $Pfx"
}
if (-not $Password) {
throw "PFX password required for $Pfx (set WIN_CODESIGN_PASSWORD or -PfxPassword)"
}
$secure = ConvertTo-SecureString $Password -AsPlainText -Force
return New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(
$Pfx, $secure, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
)
}
if ($Thumb) {
$stores = @(
"Cert:\CurrentUser\My",
"Cert:\LocalMachine\My"
)
foreach ($storePath in $stores) {
$cert = Get-ChildItem -Path $storePath -CodeSigningCert -ErrorAction SilentlyContinue |
Where-Object { $_.Thumbprint -eq $Thumb } |
Select-Object -First 1
if ($cert) { return $cert }
}
throw "Code-signing certificate not found for thumbprint: $Thumb"
}
if ($Subject) {
$stores = @(
"Cert:\CurrentUser\My",
"Cert:\LocalMachine\My"
)
foreach ($storePath in $stores) {
$cert = Get-ChildItem -Path $storePath -CodeSigningCert -ErrorAction SilentlyContinue |
Where-Object { $_.Subject -like "*$Subject*" } |
Sort-Object NotAfter -Descending |
Select-Object -First 1
if ($cert) { return $cert }
}
throw "Code-signing certificate not found in store for subject containing: $Subject"
}
return $null
}
function Expand-SignTargets {
param([string[]]$Inputs)
$targets = New-Object System.Collections.Generic.List[string]
foreach ($item in $Inputs) {
if (-not $item) { continue }
$resolved = $item
if (-not [System.IO.Path]::IsPathRooted($resolved)) {
$resolved = Join-Path (Get-Location) $resolved
}
if (-not (Test-Path -LiteralPath $resolved)) {
throw "Path not found: $item"
}
if ((Get-Item -LiteralPath $resolved).PSIsContainer) {
foreach ($ext in @("*.exe", "*.dll", "*.msi", "*.msp", "*.cab")) {
Get-ChildItem -LiteralPath $resolved -Recurse -File -Filter $ext -ErrorAction SilentlyContinue |
ForEach-Object { $targets.Add($_.FullName) }
}
} else {
$targets.Add((Resolve-Path -LiteralPath $resolved).Path)
}
}
return @($targets | Select-Object -Unique)
}
function Invoke-SignToolSign {
param(
[string]$SignTool,
[string]$Target,
[string]$Pfx,
[string]$Password,
[string]$Timestamp,
[string]$Desc,
[string]$HashAlg
)
$args = @(
"sign",
"/f", $Pfx,
"/p", $Password,
"/fd", $HashAlg,
"/tr", $Timestamp,
"/td", $HashAlg,
"/d", $Desc,
"/v",
$Target
)
Write-Host "[sign] signtool $($args -join ' ')" -ForegroundColor DarkGray
if ($DryRun) { return 0 }
$p = Start-Process -FilePath $SignTool -ArgumentList $args -Wait -PassThru -NoNewWindow
return $p.ExitCode
}
function Invoke-AuthenticodeSign {
param(
[string]$Target,
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
[string]$Timestamp,
[string]$HashAlg
)
Write-Host "[sign] Set-AuthenticodeSignature $Target" -ForegroundColor DarkGray
if ($DryRun) { return $true }
$sig = Set-AuthenticodeSignature -FilePath $Target -Certificate $Certificate `
-TimestampServer $Timestamp -HashAlgorithm $HashAlg
if ($sig.Status -ne "Valid" -and $sig.Status -ne "UnknownError") {
throw "Signing failed for $Target : $($sig.Status) - $($sig.StatusMessage)"
}
return $true
}
function Test-SignatureHealthy {
param(
[string]$Target,
[switch]$StrictTrusted
)
$sig = Get-AuthenticodeSignature -LiteralPath $Target
if ($sig.Status -eq "NotSigned") {
return @{ Ok = $false; Message = "Not signed" }
}
if ($sig.Status -eq "Valid") {
return @{ Ok = $true; Message = "Valid ($($sig.SignerCertificate.Subject))" }
}
if (-not $StrictTrusted -and $sig.SignatureType -eq "Authenticode" -and $sig.SignerCertificate) {
return @{ Ok = $true; Message = "$($sig.Status) ($($sig.SignerCertificate.Subject))" }
}
return @{ Ok = $false; Message = "$($sig.Status) - $($sig.StatusMessage)" }
}
$PfxPath = Resolve-FirstNonEmpty @(
$PfxPath,
$env:WIN_CODESIGN_PFX,
$env:HESABIX_WIN_CODESIGN_PFX
)
$PfxPassword = Resolve-FirstNonEmpty @(
$PfxPassword,
$env:WIN_CODESIGN_PASSWORD,
$env:HESABIX_WIN_CODESIGN_PASSWORD
)
$Thumbprint = Resolve-FirstNonEmpty @(
$Thumbprint,
$env:WIN_CODESIGN_THUMBPRINT
)
$SubjectContains = Resolve-FirstNonEmpty @(
$SubjectContains,
$env:WIN_CODESIGN_SUBJECT
)
if (-not $TimestampUrl) {
$TimestampUrl = Resolve-FirstNonEmpty @(
$env:WIN_CODESIGN_TIMESTAMP_URL,
"http://timestamp.digicert.com"
)
}
$targets = Expand-SignTargets -Inputs $Path
if ($targets.Count -eq 0) {
throw "No signable files found under: $($Path -join ', ')"
}
$cert = Get-CodeSignCertificate -Pfx $PfxPath -Password $PfxPassword -Thumb $Thumbprint -Subject $SubjectContains
if (-not $cert) {
throw @"
No code-signing certificate configured.
Set WIN_CODESIGN_PFX + WIN_CODESIGN_PASSWORD, or WIN_CODESIGN_THUMBPRINT, or WIN_CODESIGN_SUBJECT.
For production releases you need an OV/EV Authenticode certificate from a public CA.
"@
}
$signTool = Find-SignToolExe
$usePfxWithSignTool = [bool]($PfxPath -and $PfxPassword -and $signTool)
if ($PfxPath -and $PfxPassword -and -not $signTool) {
Write-Host "[warn] signtool.exe not found; using Set-AuthenticodeSignature fallback." -ForegroundColor Yellow
Write-Host " Install 'Windows SDK Signing Tools' for best compatibility with Advanced Installer." -ForegroundColor Yellow
}
Write-Host ""
Write-Host "Signing $($targets.Count) file(s) with: $($cert.Subject)" -ForegroundColor Cyan
Write-Host "Timestamp: $TimestampUrl"
Write-Host ""
$failures = @()
foreach ($target in $targets) {
try {
if ($usePfxWithSignTool) {
$code = Invoke-SignToolSign -SignTool $signTool -Target $target -Pfx $PfxPath `
-Password $PfxPassword -Timestamp $TimestampUrl -Desc $Description -HashAlg $HashAlgorithm
if ($code -ne 0) { throw "signtool exit $code" }
} else {
[void](Invoke-AuthenticodeSign -Target $target -Certificate $cert -Timestamp $TimestampUrl -HashAlg $HashAlgorithm)
}
$check = Test-SignatureHealthy -Target $target
if ($Verify -and -not $check.Ok) {
throw $check.Message
}
Write-Host "[ok] $target -> $($check.Message)" -ForegroundColor Green
} catch {
$failures += "$target : $_"
Write-Host "[error] $target : $_" -ForegroundColor Red
}
}
if ($failures.Count -gt 0) {
throw "Signing failed for $($failures.Count) file(s)."
}
Write-Host ""
Write-Host "Signing completed." -ForegroundColor Green
exit 0