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:
james.maesandCodex committed 2026-10-10 17:22:07 -05:00
1 parent 1461389575
commit bbf7efdba8
4 files changed
+77 -22

No files matched your search

+51 -21
View File
@@ -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'))
+15
View File
@@ -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
+4 -1
View File
@@ -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
+7
View File
@@ -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