6 Architecture
Forgejo Actions edited this page 2026-08-11 14:35:46 +00:00

🏗 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) and IV (16 random bytes) encrypt the plaintext inner zip with AES-256-CBC.
  • MacKey (32 random bytes) authenticates IV ‖ ciphertext with HMAC-SHA256; the result is written as the first 32 bytes of the file and also recorded as Mac in Detection.xml.
  • FileDigest is 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.