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 } }