A previous post, Building a Signed AutoCAD OEM Installer — Avoiding ODIS Error 15, lists three causes of Error 15: never signed, signed then modified, or a certificate problem. There is a fourth, less obvious cause: a stale entry in a file’s PE signature directory that blocks signing tools from writing a valid signature at all, even when the rest of the pipeline is correct.
The Stale Signature Directory
Every signed PE file (.exe/.dll) carries a Security Directory header entry recording where its embedded signature data lives and how large it is. In a correctly signed file, RVA + Size matches the file’s actual length exactly.
Some OEM Toolkit-generated components ship with a Security Directory entry that does not reconcile with the file’s real size — in one case partially overrunning the actual content, in another pointing entirely past end-of-file. Authenticode reports these files as unsigned, but the more important effect: no signing tool will write a fresh signature into them. The tool treats the stale entry as an existing signature and refuses to overwrite it.
This is the mechanism behind a workaround some partners already use independently: resetting the Security Directory’s RVA/Size fields in a PE editor before signing. It works, but as a manual step it is easy to skip, which is why the failure looks intermittent build to build.
Stage 5 — Detect and Repair Before Signing
Add this as a step before Stage 3 (Code Signing) in the original pipeline: scan every .exe/.dll in the Installer Wizard output for a stale Security Directory entry, and repair it before signing.
Detect
$root = "PACKAGE_ROOT_PATH"
$files = Get-ChildItem -Path $root -Recurse -Include *.exe,*.dll -File
$results = foreach ($f in $files) {
try {
$bytes = [System.IO.File]::ReadAllBytes($f.FullName)
$peOffset = [BitConverter]::ToInt32($bytes, 0x3C)
$magic = [BitConverter]::ToUInt16($bytes, $peOffset + 24)
$isPE32Plus = ($magic -eq 0x20B)
$optHeaderStart = $peOffset + 24
$dataDirStart = if ($isPE32Plus) { $optHeaderStart + 112 } else { $optHeaderStart + 96 }
$secDirOffset = $dataDirStart + (4 * 8) # IMAGE_DIRECTORY_ENTRY_SECURITY = index 4
$rva = [BitConverter]::ToUInt32($bytes, $secDirOffset)
$size = [BitConverter]::ToUInt32($bytes, $secDirOffset + 4)
[PSCustomObject]@{
Path = $f.FullName.Substring($root.Length + 1)
RVA = $rva; Size = $size; FileSize = $bytes.Length
Stale = ($rva -ne 0 -and ($rva + $size) -ne $bytes.Length)
}
} catch {
[PSCustomObject]@{ Path = $f.FullName.Substring($root.Length + 1); RVA = "N/A"; Size = "N/A"; FileSize = "N/A"; Stale = "ParseError" }
}
}
$results | Where-Object { $_.Stale -eq $true } | Format-Table -AutoSize
"Stale entries: $(($results | Where-Object { $_.Stale -eq $true }).Count) / $($results.Count) files scanned"
Repair
$path = "FULL_PATH_TO_STALE_FILE"
$bytes = [System.IO.File]::ReadAllBytes($path)
$peOffset = [BitConverter]::ToInt32($bytes, 0x3C)
$magic = [BitConverter]::ToUInt16($bytes, $peOffset + 24)
$isPE32Plus = ($magic -eq 0x20B)
$optHeaderStart = $peOffset + 24
$dataDirStart = if ($isPE32Plus) { $optHeaderStart + 112 } else { $optHeaderStart + 96 }
$secDirOffset = $dataDirStart + (4 * 8)
for ($i = 0; $i -lt 8; $i++) { $bytes[$secDirOffset + $i] = 0 }
[System.IO.File]::WriteAllBytes($path, $bytes)
Build the sign list
Not just the stale files — everything in the package that is not already validly signed.
$root = "PACKAGE_ROOT_PATH"
$files = Get-ChildItem -Path $root -Recurse -Include *.exe,*.msi,*.dll,*.cab -File
$toSign = foreach ($f in $files) {
$s = Get-AuthenticodeSignature -FilePath $f.FullName
if ($s.Status -ne 'Valid') { $f.FullName }
}
$toSign | Set-Content "OUTPUT_PATH\to_sign_list.txt" -Encoding utf8
"Files to sign: $($toSign.Count)"
Sign via a native .bat file
A PowerShell loop calling the signing tool repeatedly is not reliable at bulk scale — every call can fail identically with a spurious CreateFile error when run in a tight loop from PowerShell or a POSIX shell. A native .bat file, launched via cmd.exe, does not have this problem.
@echo off
setlocal
set "LOG=LOG_PATH\sign.log"
set "LIST=OUTPUT_PATH\to_sign_list.txt"
if exist "%LOG%" del "%LOG%"
for /f "usebackq delims=" %%F in ("%LIST%") do (
echo === %%F === >> "%LOG%"
YOUR_SIGNING_TOOL.exe --sign "%%F" >> "%LOG%" 2>&1
)
echo BAT_DONE >> "%LOG%"
endlocal
cmd.exe /c "PATH_TO\sign_all.bat"
Verify
$root = "PACKAGE_ROOT_PATH"
$files = Get-ChildItem -Path $root -Recurse -Include *.exe,*.msi,*.dll,*.cab -File
$results = foreach ($f in $files) {
$s = Get-AuthenticodeSignature -FilePath $f.FullName
[PSCustomObject]@{ Path = $f.FullName.Substring($root.Length+1); Status = $s.Status }
}
$results | Where-Object { $_.Status -ne 'Valid' } | Format-Table -AutoSize
"Valid: $(($results | Where-Object Status -eq 'Valid').Count) / $($results.Count)"
Not a One-Off
The identical defect shape — same component type, same size-mismatch magnitude — has shown up independently on more than one unrelated OEM partner’s build output. That points to the shared OEM build tooling itself, not a partner’s own build script. Filed internally for the toolchain team to track down the exact stage.
Validating the Fix
After repairing the affected files and re-signing the full package, the corrected installer was tested on a machine with Windows’ strictest code-integrity enforcement enabled (App Control for Business / WDAC) — a harder bar than a typical end-user machine. Clean install, no Error 15.
Checklist Before Shipping
- Installer Wizard completed fully with no interruptions or re-runs.
- Every
.exe/.dllin the output checked for a stale Security Directory entry, and repaired if found. - Code signing performed only after installer generation and the stale-entry repair, in that order.
- All files re-verified as
Validafter signing. - No automated step modifies, repackages, or re-wraps the files after signing.
- A clean machine installation test has passed.
Summary
Error 15 is not always a certificate or process-ordering problem. A stale PE Security Directory entry from earlier in the OEM build tooling can block signing outright, even on a correctly ordered pipeline. Detect it with a PE header check, repair it by zeroing the directory entry, then sign as usual.
Detect stale signatures → Repair → Sign → Verify → Ship.
Questions or hitting something similar? Reach out via ADN Support.

Leave a Reply