Skip to content

Treat MSI and CAB files as containers - #873

Open
bricelam wants to merge 1 commit into
dotnet:mainfrom
bricelam:msi
Open

Treat MSI and CAB files as containers#873
bricelam wants to merge 1 commit into
dotnet:mainfrom
bricelam:msi

Conversation

@bricelam

@bricelam bricelam commented May 13, 2025

Copy link
Copy Markdown

Fixes #673, fixes #874

@bricelam
bricelam requested a review from a team as a code owner May 13, 2025 20:14
@bricelam

bricelam commented May 14, 2025

Copy link
Copy Markdown
Author

Note, there's still an embedded CAB file inside the MSI that doesn't get signed. But AFAICT, it never gets laid out on disk, and its container--the MSI--gets signed anyway, so integrity is still guaranteed.

Comment thread NuGet.Config
<add key="dotnet-tools" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" />
<add key="dotnet-public" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" />
<!-- TODO: Mirror WixToolset.Dtf.WindowsInstaller.Package and dependencies -->
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be resolved before this PR is merged.

@bricelam bricelam May 14, 2025

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are the instructions for mirroring a package. Also be sure to revert update this file.

@bricelam

This comment was marked as resolved.

@bricelam

bricelam commented May 14, 2025

Copy link
Copy Markdown
Author

Fixing this will probably make #647 and #775 (resolved) more urgent for some users.

@Foda

Foda commented Jun 5, 2025

Copy link
Copy Markdown
Contributor

@bricelam I was able to verify this manually, but this only works if the .cab your .msi uses is embedded. It causes a regression with .msi installers with non-embedded .cab files (ie: #875). It throws in the wixtoolset code and appears as a "file not found" error in a temp folder.

@Foda

Foda commented Jun 10, 2025

Copy link
Copy Markdown
Contributor

@bricelam I was able to verify this manually, but this only works if the .cab your .msi uses is embedded. It causes a regression with .msi installers with non-embedded .cab files (ie: #875). It throws in the wixtoolset code and appears as a "file not found" error in a temp folder.

Just a follow-up: it's probably OK for this regression as long as it doesn't roll out in a release. We'd like to add a new flag that doesn't recurse into file containers to go along with this.

@kartheekp-ms

Copy link
Copy Markdown
Contributor

@bricelam please rebase since we migrated tests from Moq to NSubstitute.

@bricelam bricelam changed the title Treat MSI files as containers Treat MSI and CAB files as containers Sep 5, 2026
Recursively extract, sign, and re-assemble the contents of install packages and
cabinets before signing the files themselves.

Some files own others that sit beside them on disk.  An MSI owns the cabinets
that aren't embedded in it, and the payload files that aren't compressed at all.
Signer gives every input its own temporary directory, so an MSI couldn't see its
external cabinets and InstallPackage.ExtractFiles failed looking for them.  When
both the MSI and its cabinet were matched as inputs, they were signed
independently and in parallel, racing on the output path and leaving the MSI's
File table describing payloads that were never shipped.

Add ISigningDependencyReader so a format can declare the files it owns, and
ISigningDependencyProvider to resolve ownership across a set of files.  Signer
removes owned files from the input list, AggregatingSigner copies the declared
dependencies into and back out of the working directory and signs them once
their owner has rebuilt them, and MsiContainer moves the cabinets UpdateFiles
rebuilds back beside the package.

CabContainer preserves the order of the entries it repacks.  Windows Installer
requires the order of the files in a cabinet to match the order of the File
table's Sequence column, and CabInfo.Pack used the order the file system
happened to enumerate them in.

MsiCreator builds minimal install packages at test time--with an embedded or an
external cabinet--so the tests cover extraction, re-packaging, and the File table
without checking in a binary package.

ClickOnce has the same ownership problem and should move onto the same
mechanism.  See dotnet#1065.

Fixes dotnet#673
Fixes dotnet#874

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GUPH4yvFhrR4W1jPi12P5f
@bricelam

bricelam commented Sep 5, 2026

Copy link
Copy Markdown
Author

PR updated. This should be good to go now. MSI tests added.

@bricelam

bricelam commented Sep 5, 2026

Copy link
Copy Markdown
Author

From Claude Code

A note on test coverage, since one behavior here is less covered than the rest.

Two files are now signed as a unit: an MSI and any cabinet it owns. Signer fans out over its inputs in parallel, each in its own temporary directory, so before this change a glob matching both foo.msi and its data1.cab signed the cabinet twice -- once as part of the package, once as an input of its own -- racing on the output path. Because each pass produces its own timestamp countersignature, the two signed payloads differ, and the File table could end up describing a payload that was never shipped.

What is covered deterministically: SigningDependencyProviderTests covers the ownership resolution itself -- ExcludeOwnedFiles removing an owned file, handling an owner that is itself owned, and leaving unrelated files alone. MsiContainerTests and MsiSigningDependencyReaderTests cover extraction, re-packaging, the File table update, and the external-cabinet round trip against real packages built by MsiCreator. Removing the dependency copy fails 3 of the 4 end-to-end MSI tests, so the original external-cabinet regression is genuinely guarded.

What is not: SignAsync_WhenFilesAreMsiAndItsCabinet_LeavesPackageDescribingItsPayload asserts the right invariant -- the shipped payload matches the recorded FileSize and MsiFileHash -- but it still passes with the ownership pre-pass disabled. The package's own run does much more work (copy in, extract, sign, UpdateFiles, copy out) and so reliably wins the write race. The assertion is correct; it just can't be provoked by timing. Treat it as documentation of intent rather than a regression guard.

The deterministic version would assert the decision rather than the outcome: capture the container logger in the test service provider and assert that no container is ever opened for an owned cabinet. That flips reliably when the pre-pass is disabled, because it depends only on ExcludeOwnedFiles. I have not added it -- it needs a small recording ILoggerProvider in Sign.TestInfrastructure and a change to how SignerTests builds its service collection, and that felt worth agreeing on first.

Happy to add it if you would like it in this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Treat CAB files as containers Sign all unsigned assemblies in a .msi

4 participants