🏗 Architecture
Build pipeline
vendor default.nix
│ pkgs.fetchzip (SRI-pinned) → driver archive
│ unpack (and prune, if the vendor's archive ships more than one build)
▼
lib/mkIntunePackage.nix
│ merge driver files + lib/common-scripts/*.ps1 (shared PowerShell
│ helpers, e.g. Import-DriverPublisherTrust) + vendors/<v>/<s>/scripts/*.ps1
│ into one tree, then invoke `mkintunewin` (nix/intunewin-packer.nix,
│ itself wrapping tools/mkintunewin.py) on that tree
▼
$out/<name>.intunewin
flake.nix wires one packages.<system>.<vendor-series> output per
vendors/*/* folder, aggregates them all into packages.all
(packages.default), and exposes a devShell with the packer, unzip,
python3, and nix-prefetch for local iteration.
Cross-platform builds
Nothing in the pipeline needs Windows or Linux specifically: tools/mkintunewin.py
is plain Python + pycryptodome, and every Nix expression is ordinary
runCommand/fetchzip with no platform-conditional code. flake.nix uses
eachDefaultSystem, so nix build .#all produces the same result on all
four of Nix's tier-1 systems — x86_64-linux, aarch64-linux,
x86_64-darwin, aarch64-darwin — and that's verified by actually
evaluating and building on each, not just assumed from the code looking
portable.
One thing to track: nixpkgs is pinned to the nixos-26.05 stable branch,
not nixos-unstable, because unstable has already dropped x86_64-darwin
support outright (a hard eval error, not a deprecation warning) - 26.05 is
also the last stable release that will support it,
so revisit this pin if Intel Mac support ever needs to move past its
security-fix window.
CI itself specifically runs on Linux (a self-hosted Forgejo runner, Linux,
Docker-in-Docker) - that's an infrastructure choice, not a build
requirement. It does mean no Windows build agent is needed anywhere:
Microsoft's own packer, IntuneWinAppUtil.exe, is Windows-only, which is
exactly why tools/mkintunewin.py exists as a from-scratch reimplementation
in the first place.
The .intunewin format
.intunewin is Microsoft's container format for Win32 apps in Intune.
Microsoft does not publish its source or a written spec — the tool repo
ships only the compiled .exe. The description below was reconstructed from
two independent, real-world implementations that upload successfully to
Intune: svrooij/ContentPrep (C#,
cross-platform .NET tool) and community write-ups
(svrooij.io,
MSEndpointMgr).
tools/mkintunewin.py mirrors ContentPrep's Encryptor/Packager logic
byte-for-byte where it matters cryptographically.
Outer container
A .intunewin file is a plain (uncompressed) zip:
IntuneWinPackage/
├── Contents/
│ └── IntunePackage.intunewin ← encrypted inner zip (see below)
└── Metadata/
└── Detection.xml ← key material + digest, read by Intune at upload time
Inner payload and encryption
The inner IntunePackage.intunewin is: the source folder (driver files +
Install.ps1/Uninstall.ps1), zipped, then encrypted as
[ HMAC-SHA256, 32 bytes ][ IV, 16 bytes ][ AES-256-CBC ciphertext, PKCS7-padded ]
EncryptionKey(32 random bytes) andIV(16 random bytes) encrypt the plaintext inner zip with AES-256-CBC.MacKey(32 random bytes) authenticatesIV ‖ ciphertextwith HMAC-SHA256; the result is written as the first 32 bytes of the file and also recorded asMacinDetection.xml.FileDigestis the SHA-256 hash of the plaintext inner zip (computed before encryption).
All four values plus ProfileIdentifier (constant ProfileVersion1) and
FileDigestAlgorithm (constant SHA256) are written in the clear inside
Detection.xml's EncryptionInfo block — Intune needs them to decrypt the
package after upload. This is an integrity/framing mechanism, not a
confidentiality boundary: the key travels alongside the ciphertext in the
same upload. Don't read anything security-sensitive into it; the driver
payloads it wraps are ordinary vendor-published installers, and
mkIntunePackage.nix derives the key material deterministically from the
package name for reproducibility (see below), not from a secret.
Detection.xml
<ApplicationInfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" ToolVersion="1.8.6.0">
<Name>Install.ps1</Name>
<UnencryptedContentSize>123456</UnencryptedContentSize>
<FileName>IntunePackage.intunewin</FileName>
<SetupFile>Install.ps1</SetupFile>
<EncryptionInfo>
<EncryptionKey>base64...</EncryptionKey>
<MacKey>base64...</MacKey>
<InitializationVector>base64...</InitializationVector>
<Mac>base64...</Mac>
<ProfileIdentifier>ProfileVersion1</ProfileIdentifier>
<FileDigest>base64...</FileDigest>
<FileDigestAlgorithm>SHA256</FileDigestAlgorithm>
</EncryptionInfo>
</ApplicationInfo>
Name defaults to the setup file's basename; mkintunewin --name overrides
it. SetupFile is the setup file's path relative to --source.
Reproducibility
mkintunewin --seed <s> derives EncryptionKey/MacKey/IV from <s> via
HKDF-SHA256 instead of the OS random source, and all zip entries (inner and
outer) get a fixed timestamp and sorted, deterministic ordering.
lib/mkIntunePackage.nix defaults seed to the package name, so
nix build .#hp-upd-pcl6 is byte-identical across machines and CI runs as
long as the driver source and scripts don't change. Omit --seed (rare,
outside the Nix build) to get a randomly-keyed package.
Deploy-time parameterization and detection model
A package is built once per driver series. Everything that varies per
physical printer — name, IP address, optional model label for universal
drivers — is supplied as install-command arguments when an admin creates a
Win32 app instance in Intune, not baked into the package. The same
uploaded .intunewin backs every printer of that series; only the app's
command line and assignment differ.
Detection is a registry existence check, not a script:
HKLM\SYSTEM\CurrentControlSet\Control\Print\Printers\<PrinterName>. Windows
creates this key as a side effect of Add-Printer succeeding, so per-printer
detection needs no custom logic — the printer name in the detection rule and
the -PrinterName argument in the install command must simply match.
For universal drivers (HP UPD and similar), -Model is optional metadata
only (e.g. recorded as the printer's comment field for admin visibility).
The driver negotiates capabilities with the physical device over the
network port at print time; the package does not select or restrict by
model.
Format validation and fallback
tools/mkintunewin.py is an independent reimplementation, not Microsoft's
own code — but its output format is now confirmed against a real Intune
tenant: a package it built for hp-upd-pcl6 was uploaded, assigned, and
installed successfully under SYSTEM on a test device (2026-08-11). That
confirms the packaging/encryption layer; it doesn't guarantee any given
vendor's driver/script combination works on the first try — validate each
new vendor package once (see Deploying § Before rolling out
broadly).
If Intune ever rejects a package (upload error, or install failure that
traces back to a decryption/format mismatch rather than driver/script
logic): switch nix/intunewin-packer.nix to
wrap svrooij/ContentPrep (a
cross-platform .NET tool) instead of tools/mkintunewin.py, keeping the same
mkintunewin --source --setup --output [--name] [--seed] CLI contract so
lib/mkIntunePackage.nix and every vendor package need no changes.