fix(signing): reject malformed source executables
Validate owned and preserved PE headers before catalog writes. Keep non-PE package formats and unrelated excluded files outside this check. Replace initial text fixtures with real PE payloads and retain distinct culture-specific bytes. Validation: 40 signing/Azure/coverage boundary tests, changed-file PowerShell analysis, hooks and shared-policy Markdown. Exact earlier accepted malformed dependency now fails before catalog/manifest output. Real Azure signing and hosted rollout remain unproved. Refs: KofTwentyTwo/AppKit#24, KofTwentyTwo/AppKit#12, KofTwentyTwo/AppKit#20 Co-Authored-By: Codex (GPT-6) <noreply@openai.com> Signed-off-by: James Maes <james@kof22.com>
This commit is contained in:
4 files changed
+77
-22
No files matched your search
@@ -18,8 +18,8 @@ Describe 'Owned binary catalog' `
|
||||
New-Item -ItemType Directory -Path $script:Root | Out-Null
|
||||
$script:Catalog = Join-Path $script:Root 'catalog.txt'
|
||||
$script:Manifest = Join-Path $script:Root 'manifest.json'
|
||||
Set-Content -LiteralPath (Join-Path $script:Root 'Owned.dll') -Value 'owned'
|
||||
Set-Content -LiteralPath (Join-Path $script:Root 'Microsoft.dll') -Value 'third-party'
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $script:Root 'Owned.dll')
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $script:Root 'Microsoft.dll')
|
||||
}
|
||||
|
||||
It 'includes only explicit owned names and inventories other binaries' `
|
||||
@@ -51,6 +51,34 @@ Describe 'Owned binary catalog' `
|
||||
Test-Path -LiteralPath $script:Catalog | Should -BeFalse
|
||||
Test-Path -LiteralPath $script:Manifest | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'rejects malformed owned PE input before writing a catalog' `
|
||||
{
|
||||
[IO.File]::WriteAllBytes((Join-Path $script:Root 'Owned.dll'), [byte[]]::new(128))
|
||||
{ New-WindowsSigningCatalog -Root $script:Root -Patterns @('Owned.dll') -CatalogPath $script:Catalog -ManifestPath $script:Manifest } | Should -Throw '*Invalid published PE binary: Owned.dll*'
|
||||
Test-Path -LiteralPath $script:Catalog | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'rejects an already malformed preserved dependency' `
|
||||
{
|
||||
[IO.File]::WriteAllBytes((Join-Path $script:Root 'Microsoft.dll'), [byte[]]::new(128))
|
||||
{ New-WindowsSigningCatalog -Root $script:Root -Patterns @('Owned.dll') -CatalogPath $script:Catalog -ManifestPath $script:Manifest -PreserveOtherBinaries } | Should -Throw '*Invalid published PE binary: Microsoft.dll*'
|
||||
Test-Path -LiteralPath $script:Manifest | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'does not treat package-format signing targets as PE images' `
|
||||
{
|
||||
Set-Content -LiteralPath (Join-Path $script:Root 'Setup.msi') -Value 'package fixture; signature validation is a later gate'
|
||||
{ New-WindowsSigningCatalog -Root $script:Root -Patterns @('Setup.msi') -CatalogPath $script:Catalog -ManifestPath $script:Manifest } | Should -Not -Throw
|
||||
Get-Content -LiteralPath $script:Catalog | Should -Be 'Setup.msi'
|
||||
}
|
||||
|
||||
It 'ignores excluded binaries when dependency preservation is disabled' `
|
||||
{
|
||||
[IO.File]::WriteAllBytes((Join-Path $script:Root 'Microsoft.dll'), [byte[]]::new(128))
|
||||
{ New-WindowsSigningCatalog -Root $script:Root -Patterns @('Owned.dll') -CatalogPath $script:Catalog -ManifestPath $script:Manifest } | Should -Not -Throw
|
||||
@(Get-Content -LiteralPath $script:Manifest -Raw | ConvertFrom-Json) | Should -HaveCount 1
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Signature and distributed-byte gates' `
|
||||
@@ -126,8 +154,8 @@ Describe 'Signature and distributed-byte gates' `
|
||||
{
|
||||
$root = Join-Path $TestDrive 'preserve'
|
||||
New-Item -ItemType Directory -Path $root | Out-Null
|
||||
Set-Content -LiteralPath (Join-Path $root 'Owned.dll') -Value 'owned'
|
||||
Set-Content -LiteralPath (Join-Path $root 'Microsoft.dll') -Value 'third-party'
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Owned.dll')
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Microsoft.dll')
|
||||
$manifest = Join-Path $root 'manifest.json'
|
||||
New-WindowsSigningCatalog -Root $root -Patterns @('Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest -PreserveOtherBinaries
|
||||
Set-Content -LiteralPath (Join-Path $root 'Microsoft.dll') -Value 'modified'
|
||||
@@ -138,7 +166,7 @@ Describe 'Signature and distributed-byte gates' `
|
||||
{
|
||||
$root = Join-Path $TestDrive 'other-publisher'
|
||||
New-Item -ItemType Directory -Path $root | Out-Null
|
||||
Set-Content -LiteralPath (Join-Path $root 'Owned.dll') -Value 'already signed'
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Owned.dll')
|
||||
$script:ProbeSignature.SignerCertificate.SubjectName.Name = 'CN=Other Publisher'
|
||||
{ New-WindowsSigningCatalog -Root $root -Patterns @('Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath (Join-Path $root 'manifest.json') -ExpectedSubject 'CN=Verified Publisher' } | Should -Throw '*another publisher*'
|
||||
Test-Path -LiteralPath (Join-Path $root 'catalog.txt') | Should -BeFalse
|
||||
@@ -150,7 +178,7 @@ Describe 'Signature and distributed-byte gates' `
|
||||
$assets = Join-Path $root 'assets'
|
||||
$packed = Join-Path $root 'packed'
|
||||
New-Item -ItemType Directory -Path $root, $assets, $packed | Out-Null
|
||||
Set-Content -LiteralPath (Join-Path $root 'Owned.dll') -Value 'signed bytes'
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Owned.dll')
|
||||
$manifest = Join-Path $root 'manifest.json'
|
||||
New-WindowsSigningCatalog -Root $root -Patterns @('Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest
|
||||
Set-Content -LiteralPath (Join-Path $packed 'Owned.dll') -Value 'modified after signing'
|
||||
@@ -164,7 +192,7 @@ Describe 'Signature and distributed-byte gates' `
|
||||
$assets = Join-Path $root 'assets'
|
||||
$packed = Join-Path $root 'packed'
|
||||
New-Item -ItemType Directory -Path $root, $assets, $packed | Out-Null
|
||||
Set-Content -LiteralPath (Join-Path $root 'Owned.dll') -Value 'signed bytes'
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Owned.dll')
|
||||
$manifest = Join-Path $root 'manifest.json'
|
||||
New-WindowsSigningCatalog -Root $root -Patterns @('Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest
|
||||
Set-Content -LiteralPath (Join-Path $packed 'README.txt') -Value 'no binary'
|
||||
@@ -184,8 +212,8 @@ Describe 'Signature and distributed-byte gates' `
|
||||
$assets = Join-Path $root 'assets'
|
||||
$packed = Join-Path $root 'packed'
|
||||
New-Item -ItemType Directory -Path "$publish/$Head", $assets, $packed | Out-Null
|
||||
Set-Content -LiteralPath "$publish/$Head/Owned.dll" -Value 'signed app'
|
||||
Set-Content -LiteralPath "$publish/$Head/Microsoft.dll" -Value 'third-party'
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/$Head/Owned.dll"
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/$Head/Microsoft.dll"
|
||||
$manifest = Join-Path $root 'manifest.json'
|
||||
New-WindowsSigningCatalog -Root $publish -Patterns @("$Head/Owned.dll") -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest -PreserveOtherBinaries
|
||||
Copy-Item -LiteralPath "$publish/$Head/Owned.dll" -Destination $packed
|
||||
@@ -200,10 +228,10 @@ Describe 'Signature and distributed-byte gates' `
|
||||
$assets = Join-Path $root 'assets'
|
||||
$packed = Join-Path $root 'packed'
|
||||
New-Item -ItemType Directory -Path "$publish/app", $assets, $packed | Out-Null
|
||||
Set-Content -LiteralPath "$publish/app/Owned.dll" -Value 'signed app'
|
||||
Set-Content -LiteralPath "$publish/app/Microsoft.dll" -Value 'third-party'
|
||||
Set-Content -LiteralPath "$publish/app/createdump.exe" -Value 'excluded helper'
|
||||
Set-Content -LiteralPath "$publish/app/Owned.vshost.exe" -Value 'excluded host'
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/Owned.dll"
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/Microsoft.dll"
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/createdump.exe"
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/Owned.vshost.exe"
|
||||
$manifest = Join-Path $root 'manifest.json'
|
||||
New-WindowsSigningCatalog -Root $publish -Patterns @('app/Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest -PreserveOtherBinaries
|
||||
Copy-Item -LiteralPath "$publish/app/Owned.dll", "$publish/app/Microsoft.dll" -Destination $packed
|
||||
@@ -218,8 +246,8 @@ Describe 'Signature and distributed-byte gates' `
|
||||
$assets = Join-Path $root 'assets'
|
||||
$packed = Join-Path $root 'packed'
|
||||
New-Item -ItemType Directory -Path "$publish/app", $assets, $packed | Out-Null
|
||||
Set-Content -LiteralPath "$publish/app/Owned.dll" -Value 'signed app'
|
||||
Set-Content -LiteralPath "$publish/app/Microsoft.dll" -Value 'third-party'
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/Owned.dll"
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/Microsoft.dll"
|
||||
$manifest = Join-Path $root 'manifest.json'
|
||||
New-WindowsSigningCatalog -Root $publish -Patterns @('app/Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest -PreserveOtherBinaries
|
||||
Copy-Item -LiteralPath "$publish/app/Owned.dll" -Destination $packed
|
||||
@@ -233,7 +261,7 @@ Describe 'Signature and distributed-byte gates' `
|
||||
$root = Join-Path $TestDrive 'valid'
|
||||
$assets = Join-Path $root 'assets'
|
||||
New-Item -ItemType Directory -Path $root, $assets | Out-Null
|
||||
Set-Content -LiteralPath (Join-Path $root 'Owned.dll') -Value 'signed bytes'
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Owned.dll')
|
||||
$manifest = Join-Path $root 'manifest.json'
|
||||
New-WindowsSigningCatalog -Root $root -Patterns @('Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest
|
||||
$packed = Join-Path $root 'packed'
|
||||
@@ -248,8 +276,8 @@ Describe 'Signature and distributed-byte gates' `
|
||||
$root = Join-Path $TestDrive 'prefix'
|
||||
$assets = Join-Path $root 'assets'
|
||||
New-Item -ItemType Directory -Path $root, $assets | Out-Null
|
||||
Set-Content -LiteralPath (Join-Path $root 'Owned.dll') -Value 'signed core'
|
||||
Set-Content -LiteralPath (Join-Path $root 'Owned.Adapter.dll') -Value 'signed adapter'
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Owned.dll')
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination (Join-Path $root 'Owned.Adapter.dll')
|
||||
$manifest = Join-Path $root 'manifest.json'
|
||||
New-WindowsSigningCatalog -Root $root -Patterns @('Owned.dll', 'Owned.Adapter.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest
|
||||
foreach($name in @('Owned', 'Owned.Adapter'))
|
||||
@@ -268,9 +296,11 @@ Describe 'Signature and distributed-byte gates' `
|
||||
$publish = Join-Path $root 'publish'
|
||||
$assets = Join-Path $root 'assets'
|
||||
New-Item -ItemType Directory -Path $assets, "$publish/app/en", "$publish/app/fr" | Out-Null
|
||||
Set-Content -LiteralPath "$publish/app/Owned.dll" -Value 'signed app'
|
||||
Set-Content -LiteralPath "$publish/app/en/Library.resources.dll" -Value 'English'
|
||||
Set-Content -LiteralPath "$publish/app/fr/Library.resources.dll" -Value 'French'
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/Owned.dll"
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/en/Library.resources.dll"
|
||||
[IO.File]::AppendAllText("$publish/app/en/Library.resources.dll", 'English')
|
||||
Copy-Item -LiteralPath ([Reflection.PortableExecutable.PEReader].Assembly.Location) -Destination "$publish/app/fr/Library.resources.dll"
|
||||
[IO.File]::AppendAllText("$publish/app/fr/Library.resources.dll", 'French')
|
||||
$manifest = Join-Path $root 'manifest.json'
|
||||
New-WindowsSigningCatalog -Root $publish -Patterns @('app/Owned.dll') -CatalogPath (Join-Path $root 'catalog.txt') -ManifestPath $manifest -PreserveOtherBinaries
|
||||
[IO.Compression.ZipFile]::CreateFromDirectory("$publish/app", (Join-Path $assets 'Owned-Portable.zip'))
|
||||
|
||||
@@ -70,6 +70,21 @@ function New-WindowsSigningCatalog
|
||||
$null = $owned.Add($file.FullName)
|
||||
}
|
||||
}
|
||||
foreach($file in $binaries | Where-Object { ($PreserveOtherBinaries -or $owned.Contains($_.FullName)) -and $_.Extension -in '.exe', '.dll' })
|
||||
{
|
||||
$stream = [IO.File]::OpenRead($file.FullName)
|
||||
try
|
||||
{
|
||||
$reader = [Reflection.PortableExecutable.PEReader]::new($stream)
|
||||
try
|
||||
{
|
||||
if($null -eq $reader.PEHeaders.PEHeader) { throw 'Missing executable header.' }
|
||||
}
|
||||
finally { $reader.Dispose() }
|
||||
}
|
||||
catch { throw "Invalid published PE binary: $($file.Name). $($_.Exception.Message)" }
|
||||
finally { $stream.Dispose() }
|
||||
}
|
||||
$entries = @($binaries | Where-Object { $PreserveOtherBinaries -or $owned.Contains($_.FullName) } | ForEach-Object {
|
||||
[ordered]@{
|
||||
path = $_.FullName
|
||||
|
||||
@@ -154,7 +154,10 @@ The Velopack builder accepts required owned names or publish-relative patterns.
|
||||
For the starter's CLI variant, the catalog includes `app/Kof22App.exe`,
|
||||
`app/Kof22App.dll`, `app/Kof22App.Core.dll`, the three consumed AppKit DLLs and
|
||||
`cli/kof22-app.exe`; adapt these to the generated names. The no-CLI variant omits
|
||||
the last entry. Preserve all other binary bytes and signatures. The packaging hook
|
||||
the last entry. Preserve all other binary bytes and signatures. The catalog gate
|
||||
rejects malformed owned or preserved `.exe`/`.dll` inputs before catalog creation;
|
||||
unchanged hashes alone cannot prove that the source was executable. MSI/MSIX follow
|
||||
their format-specific package/signature checks. The packaging hook
|
||||
signs branded Squirrel/execution stubs and the final installer after bundling; final
|
||||
verification covers portable/CLI ZIPs, full update packages, installer signatures and
|
||||
the installer's embedded full-package hash. Manifests, checksums and attestations
|
||||
|
||||
@@ -99,6 +99,13 @@ and provenance. The prepared app builder additionally preserves dependency bytes
|
||||
verifies Velopack helpers and the installer's embedded full-package payload. These
|
||||
controls are prepared locally; Azure and live CI proof are still required.
|
||||
|
||||
Owned and preserved `.exe`/`.dll` headers are parsed before creating signing
|
||||
catalogs. An already corrupt source could otherwise retain its hash through every
|
||||
archive comparison. Regression tests reject malformed owned/dependency inputs,
|
||||
retain distinct culture-specific payloads and leave excluded/non-PE package
|
||||
formats to their applicable checks. This gate does not establish corruption's
|
||||
cause or replace final signature and payload verification.
|
||||
|
||||
## 8. Review log
|
||||
|
||||
Generated-input regression coverage for T5 and T6 lives in
|
||||
|
||||
Reference in new issue
Block a user