test: prove Authenticode release chain
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
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 }
|
||||
}
|
||||
Reference in New Issue
Block a user