The Fourth Cause of ODIS Error 15: A Stale PE Signature Directory

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/.dll in 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 Valid after 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.


Comments

Leave a Reply

Discover more from Autodesk Developer Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading