40 MSIX packages
What a program has to write so that Windows reads, installs and runs an MSIX package. Facts are tagged as in Package formats for implementers: [spec] for Microsoft Learn (package manifest and block map schemas) and ECMA-376 Part 2 (Open Packaging Conventions); [observed] for packages written by Windows' own packaging API (IAppxFactory/IAppxPackageWriter in AppxPackaging.dll, part of Windows) and for rubrapack's packages read back through IAppxPackageReader and installed with Add-AppxPackage on Windows 11. rubrapack writes what is described here.
40.1 The ZIP archive#
- The payload files first, then
AppxManifest.xml,AppxBlockMap.xmland[Content_Types].xml, in that order. [observed] - Every entry is ZIP64, whatever its size: the local header has version needed 4.5, flag 0x0008 (data descriptor), CRC and sizes 0 and no extra field; the data is followed by a ZIP64 data descriptor (
PK\7\8, CRC-32, compressed and plain size as 8 bytes each). The central directory entry has made-by and needed 4.5 (MS-DOS), sizes and offset 0xFFFFFFFF and a ZIP64 extra field (0x0001, 24 bytes: plain size, compressed size, local header offset). The archive ends with a ZIP64 end record (size 44), its locator, and an end record whose counts and offsets are all 0xFFFF / 0xFFFFFFFF. [observed] - Methods: stored (0) and deflate (8).
AppxManifest.xml,AppxBlockMap.xmland[Content_Types].xmlare deflated even when the payload is stored. [observed] - Entry names are OPC part names:
/between folders and every byte outsideA-Z a-z 0-9 - . _ ~percent-encoded, UTF-8 bytes included -data\<U+C790> %#(1).txt(a Hangul syllable, a space,%,#and parentheses) is stored asdata/%EC%9E%90%20%25%23%281%29.txt- and the UTF-8 name flag is not set.[Content_Types].xmlkeeps its brackets. [observed] - The dates in the headers are not read; Windows writes the time of writing, rubrapack 1980-01-01 00:00 so that a package does not change from one build to the next. [observed]
40.2 The block map (AppxBlockMap.xml)#
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<BlockMap xmlns="http://schemas.microsoft.com/appx/2010/blockmap"
xmlns:b4="http://schemas.microsoft.com/appx/2021/blockmap" IgnorableNamespaces="b4"
HashMethod="http://www.w3.org/2001/04/xmlenc#sha256">
<File Name="data\text.txt" Size="218890" LfhSize="43">
<Block Hash="(base64 SHA-256 of 65536 plain bytes)" Size="(compressed bytes of this block)"/>
...
<b4:FileHash Hash="(base64 SHA-256 of the whole file)"/>
</File>
...
</BlockMap>
- One
Fileper payload file and one forAppxManifest.xml; the block map and[Content_Types].xmlare not listed.Nameis the path as written, with\, not percent-encoded;Sizeis the plain size;LfhSizethe local header's size (30 + the ZIP name's length). An empty file has noBlock. [spec] [observed] - One
Blockper 65536 bytes of plain data;Hashis the base64 SHA-256 of that plain block. [spec] - A deflated file is one independent raw deflate part per block: each part decodes with a fresh decoder (nothing refers back into an earlier block) and ends with an empty stored block (
00 00 FF FF), and after the last part comes an empty final block (03 00). The block'sSizecounts the bytes of its part, so the compressed size of the file is the sum of theSizes plus 2 (an empty file: 2 bytes, no block). Stored files have noSizeon their blocks. Incompressible data is deflated as well (into stored blocks). [observed] b4:FileHashappears only for files of more than one block. [observed]- Windows' reader checks every block's hash as the file is read, and refuses a package whose file differs from its block map, both when reading and when installing. [observed]
40.3 [Content_Types].xml#
One line: a Default per file extension in order of first use (lower case), xml as application/vnd.ms-appx.manifest+xml, and Override PartName="/AppxBlockMap.xml" as application/vnd.ms-appx.blockmap+xml. rubrapack adds an Override for each file without an extension, and one for /AppxManifest.xml when a payload .xml file took the xml default. [observed]
40.4 The manifest (AppxManifest.xml)#
The smallest desktop application that Windows installs and starts (namespaces foundation/windows10, uap/windows10, restrictedcapabilities): [spec] [observed]
Identity:Name(3-50 characters ofA-Z a-z 0-9 . -),Publisher(the signing certificate's subject),Version(four parts, each at most 65535),ProcessorArchitecture(x64,arm64,x86).Properties:DisplayName,PublisherDisplayName,Logo(a PNG in the package).Dependencies/TargetDeviceFamily Name="Windows.Desktop"withMinVersionandMaxVersionTested.Resources/Resource Language.Application Id Executable EntryPoint="Windows.FullTrustApplication"withuap:VisualElements(DisplayName,Description,BackgroundColor,Square150x150Logo,Square44x44Logo), and the restricted capabilityrunFullTrust.- Paths in the manifest use
\and name files of the package.
40.5 The virtual registry (Registry.dat, User.dat)#
Registry hives at the package root (the REGF format: Registry hive files (REGF)). Measured with a packaged app on Windows 11 (26100): [observed]
Registry.dat: itsREGISTRY\MACHINE\SOFTWAREkey stands forHKLM\Software(the hive's root itself does not);REGISTRY\MACHINE\SOFTWARE\WOW6432Node\...is what the app sees in the 32-bit view. Microsoft's description ("registry.dat serves as the logical equivalent of HKLM\Software") is about what the app sees, not about the hive's layout.User.dat: its root stands forHKCU(Software\...beneath it isHKCU\Software\...).- The app reads the package's keys merged into the real registry; nothing is written to the machine's registry, and nothing is left after removal.
- Windows' packaging tools write these hives with the Offline Registry Library (its mark "OfRg" is in the base block).
40.6 The virtual file system (VFS\...)#
Files under VFS\<folder> appear to the app at the real location; the real folder does not change. Measured: ProgramFilesX64 (%ProgramFiles%), SystemX64 (System32), Common AppData (%ProgramData%) - and none for AppData or Local AppData, as Microsoft Learn says. The other names rubrapack uses (ProgramFilesX86, ProgramFilesCommonX64/X86, SystemX86, Windows) are those Microsoft Learn lists. [spec] [observed]
40.7 The resource index (resources.pri) [observed]#
ms-resource:Name in the manifest and a logo Assets\Logo.png that exists only as Assets\Logo.scale-200.png are looked up in resources.pri. No specification is published; this is the layout Windows SDK's makepri.exe writes, as far as a package needs it, and what makepri's own dump and Windows read back from rubrapack's files. All numbers little-endian; strings in qualifier and name tables UTF-16 unless noted.
- File:
mrm_pri2, u16 0, u16 1, u32 file size, u32 32 (the table of contents), u32 the first section's offset, u16 section count, u16 0xFFFF, u32 0; then one 32-byte entry per section (16-byte name, u32 0, u32 0, u32 offset from the first section, u32 length); the sections;DE FA FF DE, u32 file size,mrm_pri2. - Section: its 16-byte name, u32 0, u32 0, u32 length, u32 0, the data (padded to 8 bytes),
DE FA F5 DE, u32 length. [mrm_decn_info]- the conditions: counts (distinct qualifiers, qualifiers, qualifier sets, decisions, index entries, value characters); decisions and qualifier sets as (first index entry, count); qualifiers as (distinct qualifier, priority, score as the default x 1000, 0); distinct qualifiers as (2, type, 0, 10, u32 value offset) with types Language 0 and Scale 2; one u16 index table shared by sets (qualifier numbers) and decisions (set numbers); the values. Entry 0 of each is empty. Priorities: Language 700, Scale 200. Scores: the default language 1.0, others 0; scale 100 1.0, 125 0.937, 150 0.875, 200 0.75, 400 0.437. A decision lists its sets from the lowest score up.[mrm_pridescex]- which sections hold the schema, the decisions, the resource map and the data items.[mrm_hschemaex]- the names:ms-appx://<Identity Name>/and the name, then a tree of scopes and items (Resources/AppDisplayName,Files/Assets/Logo.png) as 12-byte entries (parent entry, full path length, first character upper-cased, name length, 0x10 scope | 0x20 ASCII name, name offset, scope or item number), each scope's children together and sorted by name; scopes (entry, child count, first child); items (entry); ASCII names. A 32-bit check value over the names is stored but was not seen checked.[mrm_res_map2_]- per item a decision and its first candidate; per candidate its value type (UTF-16 string 0, path 1, ASCII string 3, ASCII path 5) and the data item (section, index).[mrm_dataitem]- one section per qualifier set: (offset, length) pairs and the strings, each with its terminator.
40.8 Extensions#
What rubrapack writes into an application's <Extensions> (after uap:VisualElements), and only this: a source feature with no row here is an error, and one whose Windows build is above the package's MinVersion asks for min-version to be raised. [spec] Microsoft Learn, "Integrate your desktop app with Windows using packaging extensions" and the element pages. The last column is what was checked by installing a package on Windows 11 (26100). [observed]
| Source | Element (category) | Namespace | Min build | Capability | Checked on Windows 11 |
|---|---|---|---|---|---|
[assoc] | uap:Extension windows.fileTypeAssociation > uap3:FileTypeAssociation (Name, Parameters) > uap:DisplayName, uap:SupportedFileTypes > uap:FileType | uap, uap3 | 14393 | runFullTrust | opening a .rpx/.rpy file starts the program with the file |
[protocol] | uap3:Extension windows.protocol > uap3:Protocol (Name, Parameters) | uap3 | 14393 | runFullTrust | starting a scheme: URI starts the program with it |
[msix-extension] alias | uap3:Extension windows.appExecutionAlias (Executable, EntryPoint) > uap3:AppExecutionAlias > desktop:ExecutionAlias (Alias) | uap3, desktop | 14393 | runFullTrust | the alias appears in %LOCALAPPDATA%\Microsoft\WindowsApps and starts the program |
[msix-extension] startup task | desktop:Extension windows.startupTask (Executable, EntryPoint) > desktop:StartupTask (TaskId, Enabled, DisplayName) | desktop | 14393 | runFullTrust | registered after the first start |
[shortcut] Desktop | desktop7:Extension windows.shortcut > desktop7:Shortcut (File $(Desktop)\<name>.lnk, Icon, Arguments, Description) | desktop7 | 19645 | runFullTrust | the shortcut is on the user's desktop and starts the program |
[shortcut] Programs, StartMenu | none: the application's own Start entry | - | - | - | the Start menu lists the application |
[font] | uap4:Extension windows.sharedFonts > uap4:SharedFonts > uap4:Font (File Fonts\<name>), in the first application | uap4 | 15063 | - | other programs see the font while the package is installed, and not after |
- The namespaces are declared on
Package, and made ignorable, only when used, so a package without extensions keeps the manifest described above. Parameters,Arguments: literal text (MSI's[...]is refused); Windows puts the file or URI where%1is.FileTypeAssociationName: the prog-id in lower case;[assoc]tables that share a prog-id become one association with severalFileTypes.- Everything goes into the application whose executable the source names as target; a target that is not an application's executable is an error.
40.9 Bundles (.msixbundle)#
A ZIP archive laid out like a package (ZIP64 entries with data descriptors), as Windows' bundle writer (IAppxBundleWriter) makes it: [observed]
- The packages first, stored (method 0) under their file names, in the order they were added; then
AppxMetadata/AppxBundleManifest.xml,AppxBlockMap.xmland[Content_Types].xml, deflated. - The block map lists the bundle manifest only (
AppxMetadata\AppxBundleManifest.xml); the packages carry their own block maps. [Content_Types].xml:Defaultmsix=application/vnd.ms-appx,Defaultxml=application/vnd.ms-appx.bundlemanifest+xml, and anOverridefor/AppxBlockMap.xml.- The bundle manifest, CRLF line ends, tab indents, no line end after the last tag:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<Bundle xmlns="http://schemas.microsoft.com/appx/2013/bundle" SchemaVersion="5.0" xmlns:b4="http://schemas.microsoft.com/appx/2018/bundle" xmlns:b5="http://schemas.microsoft.com/appx/2019/bundle" IgnorableNamespaces="b4 b5">
<Identity Name="Example.App" Publisher="CN=Example" Version="1.0.0.0"/>
<Packages>
<Package Type="application" Version="1.0.0.0" Architecture="x64" FileName="Example.App_1.0.0.0_x64.msix" Offset="66" Size="61340">
<Resources>
<Resource Language="en-US"/>
</Resources>
<b4:Dependencies>
<b4:TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.26100.0"/>
</b4:Dependencies>
</Package>
</Packages>
</Bundle>
Offsetis where the package's bytes start in the bundle (after its local header),Sizetheir length.ResourcesandDependenciesrepeat the package manifest'sResourceandTargetDeviceFamilyelements.- The bundle's
Versionis the writer's argument (a 64-bit number, 16 bits per part); rubrapack gives the packages' version. Every package must have the bundle'sNameandPublisher, one version, and an architecture of its own. - rubrapack's bundle of the same packages has the same entries at the same offsets and the same manifest, block map and content types, byte for byte; only the ZIP dates differ (1980 in rubrapack's). Windows installs from it the package for its own architecture: x64 on an x64 machine even when x86 and arm64 are there, x86 from a bundle of x86 alone. [observed]
40.10 Signatures (AppxSignature.p7x)#
Worked out against Windows' signer (mssign32!SignerSignEx2 with APPX_SIP_CLIENT_DATA; the PowerShell cmdlet cannot sign a package) and checked by installing rubrapack's signed packages. [observed]
Publisherin the manifest must be the signing certificate's subject written the way Windows displays it: the RDNs from last to first,A=valuejoined by,- a certificate made with/CN=Example/O=Example LtdneedsO=Example Ltd, CN=Example. Otherwise signing fails with 0x8007000B.- Signing rewrites the archive the way the signer does: the payload's local records, the manifest and the block map stay as they were;
[Content_Types].xmlgets<Override PartName="/AppxSignature.p7x" ContentType="application/vnd.ms-appx.signature"/>; the signature is added last, deflated, with its sizes in the local header (version needed 2.0, no data descriptor); the central directory is written the ZIP32 way (no ZIP64 extra fields) where sizes and offsets allow, and the end record's disk numbers are 0. AppxSignature.p7xisPKCXfollowed by a CMS SignedData as in Authenticode signatures, with these differences:dataisSEQUENCE { SpcSipInfo (1.3.6.1.4.1.311.2.1.30), SEQUENCE { INTEGER 0x01010000, OCTET STRING <SIP GUID>, INTEGER 0, INTEGER 0, INTEGER 0, INTEGER 0, INTEGER 0 } }with the package SIP GUID bytes4B DF C5 0A 07 CE E2 4D B7 6E 23 C8 39 A0 9F D1or the bundle SIP GUID bytesB3 58 5F 0F DE AA 9A 4B A4 34 95 74 2D 92 EC EB; the signed attributes arecontentTypeandmessageDigestonly.- The DigestInfo's digest is not one hash but
APPXfollowed by records of a 4-byte tag and a SHA-256:AXPC: the archive from its start to the signature entry's local header;AXCD: the central directory without the signature entry, then the ZIP64 end record, its locator and the end record, all as if the signature entry did not exist (the central directory starting where the signature's local header is). Windows reads the signature entry's place from its ZIP32 fields: a signature entry described the ZIP64 way (0xFFFFFFFF there) is aHashMismatch- so is the package whose central directory is rewritten that way after signing;AXCT:[Content_Types].xml(the plain bytes);AXBM:AppxBlockMap.xml;AXCI:AppxMetadata/CodeIntegrity.cat, only when the package has one.
- A package that holds program files also gets
AppxMetadata/CodeIntegrity.cat- a catalog of them, signed by the same key - as Windows' signer adds it, and its hash asAXCI. It goes in after[Content_Types].xml(which gains an Overrideapplication/vnd.ms-pkiseccatfor it), before the signature; the block map does not list it. The catalog [observed]:- a SignedData with contentType and messageDigest as its only signed attributes, whose content (type
1.3.6.1.4.1.311.10.1, a certificate trust list) holds: the usage1.3.6.1.4.1.311.12.1.1(catalog list), a 16-byte list identifier, the time, the member algorithm1.3.6.1.4.1.311.12.1.3, the members, and two name-values (1.3.6.1.4.1.311.12.2.1:PackageFullNameandOSAttr=2:6.2, each a BMPString name, the flags0x10010001and a UTF-16 value); - per PE file two members: its SHA-1 with the member information
1.3.6.1.4.1.311.12.2.3, and its SHA-256 with that and an SpcIndirectData for a PE image; - a file's hash is its Authenticode digest, but taken over the file padded with zeros to a multiple of 8 bytes when it has no signature - as it would be before a signature is added. For a file whose length is a multiple of 8 the two are the same;
- the package full name is
<Name>_<Version>_<Architecture>_<ResourceId>_<PublisherId>, the publisher ID the first 8 bytes of the SHA-256 of the manifest'sPublisherin UTF-16LE, as 13 characters of Crockford's base32 (0-9 a-zwithouti l o u).
- a SignedData with contentType and messageDigest as its only signed attributes, whose content (type
- A bundle: every package inside is signed first (each with its own
AppxSignature.p7x), then the bundle is written around them and signed with the bundle SIP GUID, withoutAXCI. - An unsigned package that needs
-AllowUnsignedand the publisher OID (below) is not signed: signing refuses a publisher with that OID.
40.11 Installing an unsigned package#
Publishermust end inOID.2.25.311729368913984317654407730594956997722=1; thenAdd-AppxPackage -AllowUnsignedinstalls it on Windows 11. [spec]- A package with an executable needs an elevated process (otherwise 0x80073D2B: an unsigned package cannot hold an executable activation). Developer mode is not needed. [observed]
Add-AppxPackagefrom a network logon (an SSH session) fails at "PLM initialization" with 0x80070005; it works in an interactive session. [observed]- A package with file types leaves, after removal, an empty
OpenWithProgidskey underHKCU\Software\Classes\.<ext>- Windows' own doing, not the package's. [observed]
40.12 Worked example: the tutorial's hello.msix#
The MSIX of tutorial chapter 17 (--unsigned-test, x64) is 11119 bytes. It begins with the local header of its first file, hello.exe:
| Offset | Bytes | Field | Value |
|---|---|---|---|
0x00 | 50 4b 03 04 | signature | PK\3\4 |
0x04 | 2d 00 | version needed | 45 (4.5, ZIP64) |
0x06 | 08 00 | flags | 0x0008: sizes follow the data |
0x08 | 08 00 | method | 8 (deflate) |
0x0A | 00 00 21 00 | time, date | 1980-01-01 00:00 |
0x0E | 00 00 00 00 00 00 00 00 00 00 00 00 | CRC, sizes | 0 (in the data descriptor) |
0x1A | 09 00 00 00 | name, extra length | 9, 0 |
0x1E | 68 65 6c 6c 6f 2e 65 78 65 | name | hello.exe |
The 6706 deflated bytes of hello.exe follow, then its data descriptor: 50 4b 07 08 ac a3 c6 2e 32 1a 00 00 00 00 00 00 00 46 00 00 00 00 00 00 - PK\7\8, the CRC-32 2EC6A3AC, and the compressed and plain sizes as 8 bytes each (6706, 17920).
Its entry in AppxBlockMap.xml:
<File Name="hello.exe" Size="17920" LfhSize="39">
<Block Hash="K7HbHzXLkhbwTo3dcUr0vs35DlYX27qMOexDOTR8e2k=" Size="6704"/>
The file is smaller than 64 KiB, so it is one block. Its Hash is the base64 of the SHA-256 of the 17920 plain bytes (K7HbHzXLkhbwTo3dcUr0vs35DlYX27qMOexDOTR8e2k= computed here), Size the 6704 bytes of its deflate part - the compressed size 6706 minus the 2-byte final block - and LfhSize the local header's 39 bytes (30 + the 9-byte name).