From 8412408bd1c02831e08ac40b853f72ce801a8f49 Mon Sep 17 00:00:00 2001 From: bithighlander Date: Mon, 6 Jul 2026 14:13:18 -0500 Subject: [PATCH] fix(windows-installer): UseSetupLdr=no so Smart App Control can't block it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default single-exe Inno installer self-extracts an UNSIGNED setup.tmp engine to %TEMP% and runs it. Smart App Control (on by default on many Win11 machines) blocks unsigned code, so v1.4.11 died with "Setup failed to initialize" (exit 1, CodeIntegrity 3077/3033) and installed nothing. The EV signature on the outer setup.exe doesn't help — SAC evaluates the extracted setup.tmp on its own, and stock Inno can't sign it. The app binaries themselves are all EV-signed and SAC-clean; only the installer's extracted stub was the problem. Fix: - installer.iss: UseSetupLdr=no -> no setup.tmp; the signed setup.exe is the engine and runs in-process, so SAC only sees the signed exe. Inno now emits setup.exe + setup-*.bin. - build-windows-production.ps1: zip the parts into KeepKey-Vault--win-x64-setup.zip and delete the loose exe/bin so a bare (SAC-blocked) single exe can't be shipped by accident. - docs: WINDOWS-BUILD-AND-SIGN.md gains a "Smart App Control" section and a MANDATORY "install under SAC (Enforce)" checklist item — SAC-off machines are an invalid test, which is how this slipped through (1.4.7 smoke test was SAC-off). Full write-up in docs/incidents/. Validated on a SAC-Enforce machine via the real download flow (zip + MotW -> extract -> run setup.exe): exit 0, version 1.4.11, no 3077/3033, app boots. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/WINDOWS-BUILD-AND-SIGN.md | 47 ++++++++++- ....4.11-smart-app-control-installer-block.md | 80 +++++++++++++++++++ scripts/build-windows-production.ps1 | 30 ++++++- scripts/installer.iss | 6 ++ 4 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 docs/incidents/1.4.11-smart-app-control-installer-block.md diff --git a/docs/WINDOWS-BUILD-AND-SIGN.md b/docs/WINDOWS-BUILD-AND-SIGN.md index 5d6a215a..fd2f6cbe 100644 --- a/docs/WINDOWS-BUILD-AND-SIGN.md +++ b/docs/WINDOWS-BUILD-AND-SIGN.md @@ -14,7 +14,9 @@ The build, signing, and installer steps are all driven by **one PowerShell scrip .\scripts\build-windows-production.ps1 ``` -Output: `release-windows\KeepKey-Vault--win-x64-setup.exe` (signed) and `SHA256SUMS-windows.txt`. +Output: `release-windows\KeepKey-Vault--win-x64-setup.zip` (contains the +EV-signed `setup.exe` + `setup-*.bin`) and `SHA256SUMS-windows.txt`. **Ship the +`.zip`, not a bare `.exe`** — see [Smart App Control](#smart-app-control-ship-a-zip-not-a-bare-exe). To rebuild the installer from an existing build tree without rebuilding sources: @@ -153,6 +155,42 @@ After the file exists once, subsequent builds reuse it. Commit-or-don't is a sep --- +## Smart App Control: ship a `.zip`, not a bare `.exe` + +**This shipped broken in 1.4.11.** A normal single-file Inno installer +(`UseSetupLdr=yes`, the default) is a self-extractor: at runtime it unpacks an +**unsigned** `setup.tmp` engine into `%TEMP%\is-*` and executes it. **Smart App +Control (SAC)** and WDAC block unsigned code, so on any Windows 11 machine with +SAC on (the default on many clean installs) the installer dies immediately with +"Setup failed to initialize" (exit code 1) and logs **CodeIntegrity 3077/3033** +in `Microsoft-Windows-CodeIntegrity/Operational`. The outer `setup.exe` being +EV-signed does not help — SAC evaluates the extracted `setup.tmp` on its own, and +that file is never signed (stock Inno cannot sign it). + +**The fix (already in `installer.iss` + `build-windows-production.ps1`):** +`UseSetupLdr=no`. With no loader there is no `setup.tmp`; the EV-signed `setup.exe` +IS the engine and runs in-process, so SAC only sees the signed exe and allows it. +Inno then emits `setup.exe` + `setup-*.bin`, which the build script zips into +`KeepKey-Vault--win-x64-setup.zip`. Users extract the zip and run +`setup.exe`. **Only the `.zip` is uploaded; never a bare `.exe`.** + +**The app itself is SAC-clean** — every shipped binary (`KeepKeyVault.exe`, +`launcher.exe`, `bin\bun.exe`, DLLs) is EV-signed and runs under SAC without a +block. Only the installer's extracted `setup.tmp` was the problem. + +> **You MUST test the installer on a machine with Smart App Control ON (Enforce) +> before every release.** SAC-off machines install fine even with the broken +> single-exe — that is exactly how 1.4.11 slipped through (the 1.4.7 smoke test +> ran on a SAC-off box). Verify no 3077/3033 appears: +> ```powershell +> Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-CodeIntegrity/Operational';Id=3077,3033;StartTime=(Get-Date).AddMinutes(-5)} +> # (also confirm SAC is actually on: (Get-MpComputerStatus).SmartAppControlState -eq 'On') +> ``` + +Full incident write-up: [`docs/incidents/1.4.11-smart-app-control-installer-block.md`](./incidents/1.4.11-smart-app-control-installer-block.md). + +--- + ## Verifying the release After the script finishes, verify everything before uploading: @@ -265,9 +303,10 @@ Before tagging and uploading: - [ ] Run `.\scripts\build-windows-production.ps1` - [ ] **Non-AVX:** `KeepKeyVault.exe` disassembles to **0** AVX/VEX instructions (`.text` VSize ≈ `0x5A46`; `c5 f9 7f` absent) — see step 9 - [ ] **Non-AVX:** bundled `bun.exe` banner reads `Windows x64 (baseline)` and version ≥ 1.3.14 — see step 6 -- [ ] Verify installer signature via `signtool verify /pa /v` +- [ ] Verify installer signature via `signtool verify /pa /v` (on the extracted `setup.exe`) +- [ ] **Smart App Control:** extract the `.zip` and run `setup.exe` on a machine with **SAC ON (Enforce)** — installs with **no** CodeIntegrity 3077/3033. This is mandatory and non-negotiable (see [Smart App Control](#smart-app-control-ship-a-zip-not-a-bare-exe)); SAC-off machines are NOT a valid test. - [ ] Smoke-test the installer on a clean Windows VM - [ ] **Non-AVX (if hardware available):** app launches past the splash on a no-AVX CPU (Gemini Lake N5030/N4020) instead of `0xC000001D` -- [ ] Compare `SHA256SUMS-windows.txt` against the installer hash -- [ ] Upload `*.exe` and `SHA256SUMS-windows.txt` to the GitHub draft release +- [ ] Compare `SHA256SUMS-windows.txt` against the `.zip` hash +- [ ] Upload the **`.zip`** (NOT a bare `.exe`) and `SHA256SUMS-windows.txt` to the GitHub release - [ ] Run the installed app, pair a real device, confirm `vault-backend.log` has the expected boot lines diff --git a/docs/incidents/1.4.11-smart-app-control-installer-block.md b/docs/incidents/1.4.11-smart-app-control-installer-block.md new file mode 100644 index 00000000..15db8e47 --- /dev/null +++ b/docs/incidents/1.4.11-smart-app-control-installer-block.md @@ -0,0 +1,80 @@ +# Incident: 1.4.11 Windows installer blocked by Smart App Control + +**Date:** 2026-07-05 → 2026-07-06 +**Severity:** High — the Windows installer would not run for any user with Smart +App Control enabled (default on many clean Windows 11 installs). +**Status:** Fixed and re-shipped (`v1.4.11` now serves a `.zip`). + +## Summary + +The `v1.4.11` Windows installer (`KeepKey-Vault-1.4.11-win-x64-setup.exe`) failed +to launch on machines with **Smart App Control (SAC)** enabled. Double-clicking it +did nothing (or flashed an "Error" dialog); silent runs returned exit code 1 with +no Inno log ("Setup failed to initialize"). Nothing installed. + +## Impact + +- Any Windows 11 user with SAC on (a large and growing share — SAC is on by + default on many clean 24H2+ installs) could not install 1.4.11. +- Not 1.4.11-specific: **every** prior single-exe Inno release had the same latent + flaw. They "worked" only where SAC was off. + +## Root cause + +A default Inno Setup installer (`UseSetupLdr=yes`) is a self-extractor: at runtime +it unpacks an **unsigned** engine, `setup.tmp`, into `%TEMP%\is-*` and executes it. +Smart App Control / WDAC block unsigned code, so SAC killed `setup.tmp` before Inno +initialized. + +- The **outer** `setup.exe` is EV-signed and Valid — but SAC evaluates the + extracted `setup.tmp` independently, and that file is never signed. Stock Inno + (verified on 6.7.3) has no way to sign it. +- Evidence: `Microsoft-Windows-CodeIntegrity/Operational` events **3077** and + **3033**: + > Code Integrity determined that a process (…\KeepKey-Vault-1.4.11-win-x64-setup.exe) + > attempted to load …\Temp\is-*.tmp\KeepKey-Vault-1.4.11-win-x64-setup.tmp that did + > not meet the Enterprise signing level requirements … (Policy ID: {0283ac0f-…}) — the SAC base policy. +- Confirmed the extracted `setup.tmp` is `NotSigned` (for 1.4.7 and 1.4.11 alike). +- Confirmed the **app itself is SAC-clean**: `KeepKeyVault.exe`, `launcher.exe`, + `bin\bun.exe`, and DLLs are all EV-signed and run under SAC with zero blocks. + Only the installer's extracted stub was the problem. + +## Why it slipped through + +The release smoke test (done on 1.4.7 earlier in the cycle) ran on a machine with +**SAC off**, where the broken single-exe installs fine. SAC was later turned on +(enforce) on the same machine, and 1.4.11 was the first release tested there — by +a user report ("downloaded, didn't open"), not by the release checklist. The +checklist had no "install under Smart App Control" gate. + +## Fix + +`UseSetupLdr=no` in `scripts/installer.iss`. With no loader there is no +`setup.tmp`; the EV-signed `setup.exe` is the engine and runs in-process, so SAC +only sees the signed exe and allows it. Inno then emits `setup.exe` + `setup-*.bin`, +which `scripts/build-windows-production.ps1` zips into +`KeepKey-Vault--win-x64-setup.zip`. Users extract the zip and run +`setup.exe`. + +Validated on the SAC-Enforce machine (real download flow, zip marked with +Mark-of-the-Web, extracted, run): installer exit **0**, version → **1.4.11**, +**no** CodeIntegrity 3077/3033, app boots (`+869ms: boot complete`). + +`v1.4.11` was re-shipped: uploaded the `.zip`, removed the SAC-blocked `.exe`. + +## Prevention + +- `docs/WINDOWS-BUILD-AND-SIGN.md`: new "Smart App Control" section + a **mandatory + "install under SAC (Enforce)"** item in the release checklist. SAC-off machines + are explicitly called out as an invalid test. +- `build-windows-production.ps1`: auto-zips the installer parts and deletes the + loose `.exe`/`.bin` so a bare single-exe can no longer be shipped by accident. + +## Follow-ups + +- **Download page:** the Windows link must point at the `.zip` (extract → run + `setup.exe`), not a `.exe`. (Outside this repo.) +- **Long-term single-file installer:** a `.zip` is a UX regression vs a one-click + `.exe`. NSIS runs entirely in-process (no extracted stub) and would give a single + SAC-clean signed `.exe`; MSIX/WiX-MSI are also SAC-friendly. Evaluate migrating + the installer so we can go back to a single downloadable file. Tracked separately. diff --git a/scripts/build-windows-production.ps1 b/scripts/build-windows-production.ps1 index 1af32cf7..dd265e24 100644 --- a/scripts/build-windows-production.ps1 +++ b/scripts/build-windows-production.ps1 @@ -907,6 +907,30 @@ if (-not $SkipSign) { } } +# ============================================================================ +# Package as .zip (Smart App Control-safe distribution) +# ============================================================================ +# installer.iss sets UseSetupLdr=no, so Inno emits setup.exe + setup-*.bin +# instead of a single self-extracting exe. That is deliberate: the normal +# single-exe extracts an UNSIGNED setup.tmp engine to %TEMP% and runs it, which +# Smart App Control blocks ("failed to initialize", Code Integrity 3077/3033). +# With no loader there is no tmp -- SAC only sees the EV-signed setup.exe. Ship +# the parts zipped; the user extracts and runs setup.exe. See +# docs/WINDOWS-BUILD-AND-SIGN.md "Smart App Control". +Write-Step "Packaging installer as .zip (Smart App Control-safe)" +$installerParts = Get-ChildItem -Path $ArtifactsDir -File | Where-Object { + $_.Name -like "KeepKey-Vault-$Version-win-x64-setup*" -and ($_.Extension -eq '.exe' -or $_.Extension -eq '.bin') +} +if (-not $installerParts) { throw "No installer parts (setup.exe/.bin) found to package" } +$zipPath = Join-Path $ArtifactsDir "KeepKey-Vault-$Version-win-x64-setup.zip" +Remove-Item $zipPath -Force -ErrorAction SilentlyContinue +Compress-Archive -Path $installerParts.FullName -DestinationPath $zipPath -CompressionLevel Optimal +# Drop the loose parts so ONLY the .zip is uploaded — a bare setup.exe won't run +# without its .bin siblings anyway, and leaving the SAC-blocked single exe around +# invites shipping the broken artifact. +$installerParts | Remove-Item -Force +Write-Success "Created: $(Split-Path $zipPath -Leaf) ($([math]::Round((Get-Item $zipPath).Length / 1MB, 1)) MB)" + # ============================================================================ # Generate Checksums # ============================================================================ @@ -956,9 +980,9 @@ if (-not $SkipSign) { } Write-Host "" Write-Host "Next steps:" -ForegroundColor Yellow - Write-Host " 1. Test the installer: run the setup EXE" -ForegroundColor Gray - Write-Host " 2. Upload EXE to GitHub release" -ForegroundColor Gray - Write-Host " 3. Verify SmartScreen reputation" -ForegroundColor Gray + Write-Host " 1. Test on a Smart App Control (Enforce) machine: extract the .zip, run setup.exe" -ForegroundColor Gray + Write-Host " 2. Upload the .zip (NOT a bare .exe) to the GitHub release" -ForegroundColor Gray + Write-Host " 3. Verify SmartScreen/Smart App Control does not block it" -ForegroundColor Gray } else { Write-Host "WARNING: Artifacts are NOT signed - test build only" -ForegroundColor Yellow } diff --git a/scripts/installer.iss b/scripts/installer.iss index 8e828da1..e796993e 100644 --- a/scripts/installer.iss +++ b/scripts/installer.iss @@ -34,6 +34,12 @@ OutputBaseFilename=KeepKey-Vault-{#MyAppVersion}-win-x64-setup SetupIconFile={#MySourceDir}\Resources\app-real.ico Compression=lzma2 SolidCompression=yes +; Smart App Control / WDAC: a normal (UseSetupLdr=yes) single-exe installer extracts +; an UNSIGNED setup.tmp engine to %TEMP% and runs it; SAC blocks unsigned code, so the +; installer dies with "failed to initialize" (Code Integrity 3077/3033). UseSetupLdr=no +; makes the signed setup.exe the engine itself (no tmp extraction) so SAC only evaluates +; the EV-signed exe. Trade-off: output is setup.exe + setup-*.bin, shipped as a .zip. +UseSetupLdr=no WizardStyle=modern WizardImageFile={#MyScriptDir}\installer-wizard.bmp WizardSmallImageFile={#MyScriptDir}\installer-small.bmp