Repository navigation
Fix Windows TRUST_E_BAD_DIGEST on portable-signed MSIX/AppX; sign plain-ZIP packages - #46
Merged
Marc-André Moreau (mamoreau-devolutions) merged 3 commits intoNov 2, 2026
Conversation
Windows AppxSip reported TRUST_E_BAD_DIGEST (0x80096010) for MSIX/AppX packages produced by `psign-tool --mode portable sign`, while psign's own verify-msix accepted them. The staged and final packages were written by the zip crate, whose layout differs from makeappx/signtool in three ways, each of which alone makes Windows reject the digest: - only a classic EOCD (no ZIP64 EOCD record + locator); - central directory "version made by" 0x032e instead of 45; - external attributes 0x81a40000 instead of 0. Add msix_digest::normalize_zip_for_appx_signing and pass the staged flat and bundle packages and the final AppxSignature.p7x embed through it. Make the portable AXCD digest reject layouts Windows rejects, so psign verify no longer passes packages that fail natively. Parse and validate the SDK 26100 `b4:FileHash` block-map element, which previously made verify-msix fail on modern signtool output. Add a Windows test that portable-signs a makeappx-built package with a throwaway in-memory PFX and asserts WinVerifyTrust fails only with CERT_E_UNTRUSTEDROOT, plus portable layout and FileHash unit tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitHub Actions runs cargo under pwsh 7, whose inherited PSModulePath makes Windows PowerShell 5.1 fail to autoload Microsoft.PowerShell.Security (Get-AuthenticodeSignature: CouldNotAutoloadMatchingModule). Drop the inherited PSModulePath and import the module from $PSHOME explicitly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Publishers that drop makeappx (pinget's source2.msix) hand psign a plain ZIP of the package layout. Signing those failed when [Content_Types].xml came from a generic tool (no AppxBlockMap.xml override) or was missing, and every payload was stored uncompressed (~10x larger than makeappx). - appx_zip: write packages in the AppxPackaging layout directly, deflating payloads with a full flush per 64 KiB block (each block decodes on its own) and listing compressed block sizes in AppxBlockMap.xml. Already compressed formats, incompressible data, and bundle children stay stored. The final p7x embed copies other entries' compressed bytes. - msix_digest::appx_content_types_for_signing: merge or generate [Content_Types].xml with makeappx-style defaults, the manifest type, and the block map / signature overrides. Tested on Windows 11 / SDK 26100 with pinget-shaped packages signed on Windows and Linux, with and without RFC 3161 timestamps: WinVerifyTrust fails only with CERT_E_UNTRUSTEDROOT and makeappx unpack accepts them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Marc-André Moreau (mamoreau-devolutions)
merged commit Nov 2, 2026
157cf1a
into
master
36 checks passed
Marc-André Moreau (mamoreau-devolutions)
deleted the
claude/fix-msix-zip64-eocd-531450
branch
November 2, 2026 21:42
Marc-André Moreau (mamoreau-devolutions)
added a commit
that referenced
this pull request
Nov 2, 2026
## Summary - Bump all workspace packages and `Cargo.lock` from 0.8.1 to 0.8.2 using `scripts/bump-version.ps1`. - Synchronize the PowerShell module, NuGet tool fallback version, README packaging example, and release workflow example. - Prepare version metadata only. This PR does not dispatch the release workflow or publish packages. **Merge after #46.** 0.8.2 exists to ship that fix: portable-signed MSIX/AppX packages that Windows rejected with `TRUST_E_BAD_DIGEST`, plus signing of plain-ZIP packages for pinget's `source2.msix`. The two PRs merge cleanly in either order, and the combined `Cargo.lock` passes `cargo metadata --locked`. ## Validation - `pwsh ./scripts/bump-version.ps1 -Version 0.8.2 -Check` - `cargo metadata --locked --no-deps --format-version 1` - `git diff --check` - Test merge with `claude/fix-msix-zip64-eocd-531450`: clean, and `-Check` / `--locked` still pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Marc-André Moreau (mamoreau-devolutions)
added a commit
to Devolutions/jiffshot
that referenced
this pull request
Nov 2, 2026
## Summary - Update the Windows signing action to psign 0.8.2 and pin the published Windows x64 archive's SHA-256. - Include Devolutions/psign#46, which fixes the Windows hash mismatch seen when verifying portable-signed MSIX packages in the failed release job. ## Verification - Verified the downloaded release archive's SHA-256 and executable version. - A new release workflow run after merging is needed to validate Azure Trusted Signing end to end; rerunning the failed job would use its original revision. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Marc-André Moreau (mamoreau-devolutions)
added a commit
to Devolutions/pinget
that referenced
this pull request
Nov 3, 2026
…tool (#60) ## Summary Follow-up to #59. The WinGet source publishers no longer need the Windows SDK (`makeappx.exe`, `signtool.exe`) or, in Rust, PowerShell. - **Packaging:** both publishers write `source2.msix` in-process, in the exact AppxPackaging (makeappx) ZIP layout: - stored entries; - central directory entries with version-made-by 45 and zero external attributes; - ZIP64 end records; - an SHA-256 block map with a hash per 64 KiB block; - content types. Windows AppxSip rejects signatures over any other layout with `TRUST_E_BAD_DIGEST`. The Rust (`msix.rs`) and .NET (`MsixPackageWriter.cs`) writers are byte-identical for the same payload, and a .NET test pins hashes produced by the Rust writer. - **Signing:** done with the cross-platform [psign-tool](https://github.com/Devolutions/psign) (`Devolutions.Psign.Tool`, **0.8.2 or later**; 0.8.1's portable MSIX signatures fail Windows verification, fixed in Devolutions/psign#46). - All arguments, including the PFX password, go through a private, short-lived response file, so the password never appears on a command line. That matters on Linux, where `/proc/*/cmdline` is readable by other users, and for the `.cmd` launcher `dotnet tool install` creates on Windows. - **Verification**, after signing and in `verify --destination`: - the package digests; - the signer subject matches `--publisher`, formatted the way Windows formats certificate subjects; - the signer has the Code Signing EKU and is currently valid; - chain trust: the Windows store on Windows (as `signtool verify /pa` did), or new repeatable `--trusted-ca` anchors. - **CLI:** `--makeappx`/`--signtool` become `--psign-tool`; `doctor` JSON reports `psignToolPath`. - **Portability:** `publish` and `verify` now run off Windows, using the existing portable MSZIP decoders. `build` still compresses with the Windows Compression API. - **CI, scripts and docs:** - CI installs psign-tool 0.8.2. - The gate scripts take `-PsignToolPath`. - The interoperability check no longer needs makeappx. - Docs are updated. ## Validation (local, Windows 11, SDK 10.0.26100) - **Rust:** publisher tests pass (19 unit + 26 conformance), as do the CLI tests; `clippy` and `fmt` are clean. - **.NET:** the solution builds; 80/80 publisher tests pass. - **Interop:** `tests/publisher/run-production-conformance.ps1` passes: Rust and .NET reopen each other's state and verify each other's unsigned feeds. - **Signed end to end** with released psign-tool 0.8.2, both CLIs, via the plain exe and the dotnet-tool `.cmd` launcher, with a password containing quotes, `@` and `%PATH%`: - `signtool verify /pa` and `Get-AuthenticodeSignature` report only the untrusted throwaway root, never a bad digest; - `--trusted-ca` publish and verify succeed; - a mismatched `--publisher` is rejected; - `makeappx unpack` accepts both signed and unsigned output. - **Not yet run:** the signed stock-WinGet acceptance job, which runs in CI on this PR. Note that NuGet had not yet listed the 0.8.2 platform packages (`Devolutions.Psign.Tool.win-x64`, …) shortly after the release. If they're still missing, the job's install step will fail until NuGet finishes indexing. ## Notes - The full Rust and .NET `source2.msix` still differ, because their manifest text and generated logos differ (unchanged from before). Only the container layout is identical. - `tests/publisher/build_v2_fixture.ps1` still uses makeappx on purpose. It's a manual, independent reference-fixture builder, and nothing calls it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
psign-tool --mode portable signproduced MSIX/AppX packages that psign's owntrust-verify-msixaccepted, but that Windows rejected:signtool verify /pa→0x80096010(TRUST_E_BAD_DIGEST)Get-AuthenticodeSignature→HashMismatchThe same unsigned package signed by
signtool.exewith 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
zip0.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:zipcrate0xFFclassic EOCD0x032e(Unix, 4.6)0x002d(45)0x81a40000(0o100644)0I 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 by20 /0x0314/ 46 and external attrs0x20(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: newmsix_digest::normalize_zip_for_appx_signingrewrites 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) andreplace_msix_signature_partpass their output through it. The digest is computed over the same layout that gets shipped.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 failtrust-verify-msixwith an explicit message instead of passing.b4:FileHashelement (http://schemas.microsoft.com/appx/2021/blockmap), which holds the digest of the whole uncompressed file. Before this,verify-msixfailed on modern signtool output withunexpected AppxBlockMap.xml element `FileHash`.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].xmlfrom generic tools broke signing (content type for `AppxBlockMap.xml` is `application/vnd.ms-appx.manifest+xml`), and a ZIP with no[Content_Types].xmlwas refused.Changes:
psign-portable-core::appx_zip: a small ZIP writer that emits the AppxPackaging layout directly. It replaces thezip-crate writer for staging and for the finalAppxSignature.p7xembed, 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 intoBlock/@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-styleDefaults for payload extensions, forces the (bundle) manifest type, and adds the block map and signatureOverrides.docs/linux-signing-pipelines.md(MSIX without makeappx) anddocs/rust-sip-gaps.md.Checked on Windows 11 / SDK 26100 with a pinget-shaped package (neutral
windows.appExtensionmanifest, a 1.3 MBPublic/index.db, PNG assets), signed on Windows and on Linux (WSL Ubuntu 24.04), with and without RFC 3161 timestamping (DigiCert TSA):[Content_Types].xml[Content_Types].xml0x800B0109 means only the self-signed root is untrusted. Timestamped signatures show
The signature is timestampedinsigntool 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):makeappx pack;CertificateRequest(never imported into any store);psign-tool verify /pa) reports0x800B0109, never0x80096010, thatGet-AuthenticodeSignaturereportsUnknownError(notHashMismatch), and thattrust-verify-msixpasses.psign-portable-core::staged_and_signed_msix_use_appx_zip64_layout: staged and signed.msix/.appx/.msixbundleoutputs have ZIP64 end records and normalized CD entries, checked by an independent byte parser.makeappx unpackto accept every signed output.psign-portable-core::signs_plain_zip_package_without_makeappx_metadata: a plain deflated ZIP with no[Content_Types].xmlsigns into a compressed package with per-blockSizeattributes and generated content types.appx_zipunit tests: each deflated block decodes independently, incompressible/precompressed data is stored, and output round-trips through thezipreader.msix_digest::content_types_for_signing_fill_gaps_and_keep_existing_typescovers the content-types builder.psign-sip-digest: unit tests for the normalizer (layout, idempotence, unchanged offsets), for the verify-side rejections, and forFileHashparse/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:
The portable-signed
.msixbundle(with a portable-signed child) also fails only on the untrusted root. All committed signed MSIX/AppX fixtures still passverify-msix.cargo fmt --all,cargo clippy --workspace --all-targets --locked: clean.cargo test --workspace --locked --no-fail-fast: all MSIX tests pass. Locally, 5corpus_sign_verifytests fail with0x800B0109on PE/ZIP-authenticode fixtures because this machine does not trust the Devolutions test root (CI bootstraps it viabootstrap-devolutions-authenticode.ps1). Those failures are unrelated to this change.🤖 Generated with Claude Code