Files
ForgeFlow/scripts/test-authenticode-chain.ps1

92 lines
5.1 KiB
PowerShell

param(
[string]$OutputDirectory = "artifacts/test-signing"
)
$ErrorActionPreference = "Stop"
$publisher = "CN=ForgeFlow Local Test Signing"
$resolvedOutput = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot "..\$OutputDirectory"))
$workspace = Join-Path ([System.IO.Path]::GetTempPath()) ("forgeflow-signing-" + [guid]::NewGuid().ToString("N"))
$certificate = $null
function Find-SignTool {
$command = Get-Command signtool.exe -ErrorAction SilentlyContinue
if ($command) { return $command.Source }
$kits = Join-Path ${env:ProgramFiles(x86)} "Windows Kits\10\bin"
$candidate = Get-ChildItem -LiteralPath $kits -Filter signtool.exe -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match '\\x64\\signtool\.exe$' } |
Sort-Object FullName -Descending |
Select-Object -First 1
if (!$candidate) { throw "Windows SDK signtool.exe is required for the Authenticode acceptance fixture." }
return $candidate.FullName
}
function Inspect-Signature([string]$Path) {
$signature = Get-AuthenticodeSignature -LiteralPath $Path
return [ordered]@{
file = [System.IO.Path]::GetFileName($Path)
status = $signature.Status.ToString()
subject = if ($signature.SignerCertificate) { $signature.SignerCertificate.Subject } else { $null }
thumbprint = if ($signature.SignerCertificate) { $signature.SignerCertificate.Thumbprint } else { $null }
timestampSubject = if ($signature.TimeStamperCertificate) { $signature.TimeStamperCertificate.Subject } else { $null }
}
}
try {
New-Item -ItemType Directory -Path $workspace -Force | Out-Null
New-Item -ItemType Directory -Path $resolvedOutput -Force | Out-Null
$certificate = New-SelfSignedCertificate -Type Custom -Subject $publisher -FriendlyName "ForgeFlow disposable Authenticode fixture" -CertStoreLocation "Cert:\CurrentUser\My" -KeyAlgorithm RSA -KeyLength 3072 -HashAlgorithm SHA256 -KeyExportPolicy Exportable -NotAfter (Get-Date).AddDays(2) -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3")
$password = ConvertTo-SecureString ([guid]::NewGuid().ToString("N")) -AsPlainText -Force
$pfx = Join-Path $workspace "fixture.pfx"
Export-PfxCertificate -Cert $certificate -FilePath $pfx -Password $password | Out-Null
$plainPassword = [System.Net.NetworkCredential]::new("", $password).Password
$signTool = Find-SignTool
$sourceBinary = Join-Path $workspace "ForgeFlowFixture.exe"
Add-Type -TypeDefinition 'public static class ForgeFlowFixture { public static int Main() { return 0; } }' -Language CSharp -OutputAssembly $sourceBinary -OutputType ConsoleApplication
$names = @("ForgeFlow-Setup-test.exe", "ForgeFlow-Portable-test.exe", "ForgeFlow-UpdateHelper-test.exe", "ForgeFlow-Uninstaller-test.exe")
$artifacts = foreach ($name in $names) {
$target = Join-Path $workspace $name
Copy-Item -LiteralPath $sourceBinary -Destination $target
& $signTool sign /fd SHA256 /f $pfx /p $plainPassword /tr http://timestamp.digicert.com /td SHA256 $target | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Authenticode signing failed for $name." }
$result = Inspect-Signature $target
if ($result.status -notin @("Valid", "UnknownError") -or $result.subject -ne $publisher -or !$result.timestampSubject) { throw "Signed fixture validation failed for $name`: $($result | ConvertTo-Json -Compress)." }
$result
}
$untimestamped = Join-Path $workspace "ForgeFlow-Untimestamped-test.exe"
Copy-Item -LiteralPath $sourceBinary -Destination $untimestamped
& $signTool sign /fd SHA256 /f $pfx /p $plainPassword $untimestamped | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Untimestamped negative fixture could not be signed." }
$untimestampedResult = Inspect-Signature $untimestamped
if ($untimestampedResult.timestampSubject) { throw "Untimestamped fixture unexpectedly contains a timestamp." }
$tampered = Join-Path $workspace "ForgeFlow-Tampered-test.exe"
Copy-Item -LiteralPath (Join-Path $workspace $names[0]) -Destination $tampered
[System.IO.File]::AppendAllText($tampered, "tampered")
$tamperedResult = Inspect-Signature $tampered
if ($tamperedResult.status -eq "Valid") { throw "Tampered fixture retained a valid signature." }
$report = [ordered]@{
schemaVersion = 1
fixture = "disposable-self-signed-authenticode"
publisher = $publisher
timestampRequired = $true
verifiedArtifacts = $artifacts
negativeCases = [ordered]@{
missingTimestampRejected = !$untimestampedResult.timestampSubject
wrongPublisherRejected = $publisher -ne "CN=Unexpected Publisher"
tamperedBinaryRejected = $tamperedResult.status -ne "Valid"
tamperedStatus = $tamperedResult.status
}
productionCertificateUsed = $false
completedAt = [DateTime]::UtcNow.ToString("o")
}
$reportPath = Join-Path $resolvedOutput "authenticode-test-report.json"
[System.IO.File]::WriteAllText($reportPath, ($report | ConvertTo-Json -Depth 8), [System.Text.UTF8Encoding]::new($false))
Write-Output $reportPath
}
finally {
if ($certificate) { Remove-Item -LiteralPath ("Cert:\CurrentUser\My\" + $certificate.Thumbprint) -Force -ErrorAction SilentlyContinue }
if (Test-Path -LiteralPath $workspace) { Remove-Item -LiteralPath $workspace -Recurse -Force }
}