Repository navigation
Commit 157cf1a
Fix Windows TRUST_E_BAD_DIGEST on portable-signed MSIX/AppX; sign plain-ZIP packages (#46)
## Problem
`psign-tool --mode portable sign` produced MSIX/AppX packages that
psign's own `trust-verify-msix` accepted, but that Windows rejected:
- `signtool verify /pa` → `0x80096010` (`TRUST_E_BAD_DIGEST`)
- `Get-AuthenticodeSignature` → `HashMismatch`
The same unsigned package signed by `signtool.exe` with the same
self-signed PFX only fails on the untrusted root.
## Root cause
The staged package and the final package are both rebuilt with the `zip`
0.6 crate. Its layout differs from AppxPackaging (makeappx/signtool) in
three ways, and Windows AppxSip rejects a package that differs in
**any** of them, even when the portable AXCD digest is self-consistent:
| Field | `zip` crate | AppxPackaging |
|---|---|---|
| End records | classic EOCD only | ZIP64 EOCD (v45) + locator +
all-`0xFF` classic EOCD |
| CD "version made by" | `0x032e` (Unix, 4.6) | `0x002d` (45) |
| CD external attributes | `0x81a40000` (`0o100644`) | `0` |
I tested this on Windows 11 / SDK 10.0.26100. Each row was toggled on
its own, and the only combination Windows accepted was all three
AppxPackaging values. I also tried `version made by` 20 / `0x0314` / 46
and external attrs `0x20` (`FILE_ATTRIBUTE_ARCHIVE`): Windows rejected
all of them. Hashing synthesized AppxPackaging values over a
non-conforming archive doesn't match Windows either, so Windows isn't
normalizing these fields. It requires them.
## Changes
- **`psign-sip-digest`**: new
`msix_digest::normalize_zip_for_appx_signing` rewrites a ZIP into the
AppxPackaging layout. It sets "version made by" to 45 and external attrs
to 0 on every CD entry, and replaces the end records with ZIP64 EOCD +
locator + sentinel classic EOCD. Local headers and data stay untouched,
so entry offsets don't move.
- **`psign-portable-core`**: the flat and bundle staging
(`stage_flat_msix_for_signature`, `stage_msix_bundle_for_signature`) and
`replace_msix_signature_part` pass their output through it. The digest
is computed over the same layout that gets shipped.
- **Portable verify** (`hash_cd_and_eocd`): rejects packages Windows
rejects, meaning those with no ZIP64 EOCD or with non-AppxPackaging
"version made by" / external attributes. Packages written by psign ≤
0.8.1 now fail `trust-verify-msix` with an explicit message instead of
passing.
- **Block map**: parses and validates the SDK 26100+ `b4:FileHash`
element (`http://schemas.microsoft.com/appx/2021/blockmap`), which holds
the digest of the whole uncompressed file. Before this, `verify-msix`
failed on modern signtool output with ``unexpected AppxBlockMap.xml
element `FileHash` ``.
- Docs: `docs/rust-sip-gaps.md`.
## Signing packages not built by makeappx (pinget `source2.msix`)
Pinget is dropping makeappx, so its input to psign will be a plain ZIP
of the package layout. With only the layout fix above, signing such
packages still failed or lost compression:
- `[Content_Types].xml` from generic tools broke signing (``content type
for `AppxBlockMap.xml` is `application/vnd.ms-appx.manifest+xml` ``),
and a ZIP with no `[Content_Types].xml` was refused.
- psign stored every payload uncompressed, so a package makeappx would
write at ~145 KB came out around 1.4 MB.
Changes:
- **`psign-portable-core::appx_zip`**: a small ZIP writer that emits the
AppxPackaging layout directly. It replaces the `zip`-crate writer for
staging and for the final `AppxSignature.p7x` embed, which now copies
the other entries' compressed bytes verbatim. Payloads are deflated as
makeappx does: a full flush after every 64 KiB block, so each block
decodes independently, followed by an empty final block, and each
block's compressed size goes into `Block/@Size`.
PNG/JPEG/ZIP/package/media payloads, payloads that compression wouldn't
shrink, and bundle children are stored.
- **`msix_digest::appx_content_types_for_signing`**: merges or generates
`[Content_Types].xml`. It keeps the input's types, adds makeappx-style
`Default`s for payload extensions, forces the (bundle) manifest type,
and adds the block map and signature `Override`s.
- Docs: `docs/linux-signing-pipelines.md` (MSIX without makeappx) and
`docs/rust-sip-gaps.md`.
Checked on Windows 11 / SDK 26100 with a pinget-shaped package (neutral
`windows.appExtension` manifest, a 1.3 MB `Public/index.db`, PNG
assets), signed on **Windows and on Linux (WSL Ubuntu 24.04)**, with and
without RFC 3161 timestamping (DigiCert TSA):
| Input | Signed size | WinVerifyTrust | Get-AuthenticodeSignature |
makeappx unpack | trust-verify-msix |
|---|---|---|---|---|---|
| generic deflated ZIP + `[Content_Types].xml` | 148 KB | 0x800B0109 |
UnknownError | ok | ok |
| plain ZIP, no `[Content_Types].xml` | 148 KB | 0x800B0109 |
UnknownError | ok | ok |
| makeappx-built pinget layout (145 KB unsigned) | 148 KB | 0x800B0109 |
UnknownError | ok | ok |
| makeappx-built minimal package with a PE | 42 KB | 0x800B0109 |
UnknownError | ok | ok |
0x800B0109 means only the self-signed root is untrusted. Timestamped
signatures show `The signature is timestamped` in `signtool verify /pa
/v`.
## Tests
-
`tests/msix_native_makeappx.rs::portable_signed_makeappx_msix_fails_windows_verify_only_on_untrusted_root`
(Windows CI, existing "Portable MSIX/AppX native makeappx.exe
validation" step):
- builds an MSIX with `makeappx pack`;
- creates a throwaway self-signed code-signing PFX in memory with
`CertificateRequest` (never imported into any store);
- portable-signs it with SHA256 and SHA384;
- asserts that WinVerifyTrust (`psign-tool verify /pa`) reports
`0x800B0109`, never `0x80096010`, that `Get-AuthenticodeSignature`
reports `UnknownError` (not `HashMismatch`), and that
`trust-verify-msix` passes.
- `psign-portable-core::staged_and_signed_msix_use_appx_zip64_layout`:
staged and signed `.msix` / `.appx` / `.msixbundle` outputs have ZIP64
end records and normalized CD entries, checked by an independent byte
parser.
- The Windows test also signs a plain ZIP of the same layout (no block
map, no content types) and requires `makeappx unpack` to accept every
signed output.
-
`psign-portable-core::signs_plain_zip_package_without_makeappx_metadata`:
a plain deflated ZIP with no `[Content_Types].xml` signs into a
compressed package with per-block `Size` attributes and generated
content types.
- `appx_zip` unit tests: each deflated block decodes independently,
incompressible/precompressed data is stored, and output round-trips
through the `zip` reader.
`msix_digest::content_types_for_signing_fill_gaps_and_keep_existing_types`
covers the content-types builder.
- `psign-sip-digest`: unit tests for the normalizer (layout,
idempotence, unchanged offsets), for the verify-side rejections, and for
`FileHash` parse/validate (match, mismatch, wrong length, duplicate).
## Verification (Windows 11, SDK 10.0.26100)
makeappx-built MSIX, portable-signed by this branch with a throwaway
self-signed PFX:
```text
signtool verify /pa -> SignTool Error: A certificate chain processed, but terminated in a root
certificate which is not trusted by the trust provider.
psign-tool verify /pa (WinVerifyTrust) -> 0x800B0109 (was 0x80096010 before this change)
Get-AuthenticodeSignature -> UnknownError (was HashMismatch)
psign-tool portable trust-verify-msix -> ok
```
The portable-signed `.msixbundle` (with a portable-signed child) also
fails only on the untrusted root. All committed signed MSIX/AppX
fixtures still pass `verify-msix`.
`cargo fmt --all`, `cargo clippy --workspace --all-targets --locked`:
clean. `cargo test --workspace --locked --no-fail-fast`: all MSIX tests
pass. Locally, 5 `corpus_sign_verify` tests fail with `0x800B0109` on
PE/ZIP-authenticode fixtures because this machine does not trust the
Devolutions test root (CI bootstraps it via
`bootstrap-devolutions-authenticode.ps1`). Those failures are unrelated
to this change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>1 parent ea9b61f commit 157cf1a
8 files changed
Lines changed: 1310 additions & 115 deletions
File tree
- crates
- psign-portable-core
- src
- psign-sip-digest/src
- docs
- tests
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
| 32 | + | |
32 | 33 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
0 commit comments