92 lines
5.1 KiB
PowerShell
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 }
|
|
}
|