fix(intunewin): align Detection.xml with IntuneWinAppUtil.exe reference format

Four structural defects in the generated Detection.xml caused Intune's upload
wizard to silently fail metadata parsing (empty form, OK button greyed):

1. Missing ToolVersion="1.8.6.0" XML attribute on ApplicationInfo — the wizard
   uses this to validate the package was produced by a compatible tool.
2. Spurious xmlns="http://schemas.microsoft.com/IntuneWin" namespace — changes
   element identity for Intune's XML parser (reference emits no namespace).
3. <?xml version="1.0" ?> declaration header — reference uses OmitXmlDeclaration=true.
4. Extra <MacAlgorithm> child element inside EncryptionInfo — not present in
   the reference FileEncryptionInfo model (svrooij/ContentPrep verified).

Fix: switched from toprettyxml() to tostring(xml_declaration=False)+indent(),
added ToolVersion attribute, removed xmlns and MacAlgorithm.
Tests updated to assert the corrected reference format; all 114 pass.

Root cause verified against svrooij/ContentPrep Packager.cs + ApplicationInfo.cs
(open-source C# reference implementation of IntuneWinAppUtil.exe).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-13 11:48:36 +02:00
co-authored by Claude Sonnet 4.6
parent 46cfde00e0
commit 77162466d5
2 changed files with 59 additions and 32 deletions
+23 -7
View File
@@ -11,6 +11,12 @@ Encrypted blob layout (from svrooij.io reverse-engineering):
IMPORTANT: IV is 16 bytes, NOT 32. STACK.md has a documentation error on this point.
Detection.xml format matches the reference IntuneWinAppUtil.exe output exactly:
- ToolVersion is an XML *attribute* on <ApplicationInfo> (not a child element)
- No xmlns namespace declaration (reference uses [XmlRoot("ApplicationInfo")] with no namespace)
- No <?xml ...?> declaration header (reference uses OmitXmlDeclaration=true)
- No <MacAlgorithm> element (not present in reference FileEncryptionInfo model)
Outer ZIP structure:
IntuneWinPackage/
├── Contents/
@@ -24,13 +30,18 @@ import hmac
import io
import os
import zipfile
import xml.dom.minidom
from xml.etree.ElementTree import Element, SubElement, tostring
from xml.etree.ElementTree import Element, SubElement, indent, tostring
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
# Version string that matches the reference IntuneWinAppUtil.exe tool.
# Intune's upload wizard validates or uses this field to confirm the package
# was produced by a compatible tool version.
_TOOL_VERSION = "1.8.6.0"
def build_intunewin(source_dir: str, setup_file: str, output_path: str) -> None:
"""Build a .intunewin file from source_dir, with setup_file as entry point.
@@ -76,9 +87,14 @@ def build_intunewin(source_dir: str, setup_file: str, output_path: str) -> None:
file_digest = hashlib.sha256(plaintext).digest()
# --- Step 7: Build Detection.xml ---
# Format MUST match IntuneWinAppUtil.exe reference output exactly:
# - ToolVersion is an XML attribute on ApplicationInfo (not a child element)
# - No xmlns namespace (reference omits it)
# - No <?xml?> declaration header
# - No MacAlgorithm element (not in reference FileEncryptionInfo model)
app_info = Element(
"ApplicationInfo",
attrib={"xmlns": "http://schemas.microsoft.com/IntuneWin"},
attrib={"ToolVersion": _TOOL_VERSION},
)
SubElement(app_info, "Name").text = setup_file
SubElement(app_info, "UnencryptedContentSize").text = str(len(plaintext))
@@ -90,14 +106,14 @@ def build_intunewin(source_dir: str, setup_file: str, output_path: str) -> None:
SubElement(enc_info, "MacKey").text = base64.b64encode(mac_key).decode()
SubElement(enc_info, "InitializationVector").text = base64.b64encode(iv).decode()
SubElement(enc_info, "Mac").text = base64.b64encode(mac_digest).decode()
SubElement(enc_info, "MacAlgorithm").text = "SHA256"
SubElement(enc_info, "ProfileIdentifier").text = "ProfileVersion1"
SubElement(enc_info, "FileDigest").text = base64.b64encode(file_digest).decode()
SubElement(enc_info, "FileDigestAlgorithm").text = "SHA256"
detection_xml = xml.dom.minidom.parseString(
tostring(app_info, encoding="unicode")
).toprettyxml(indent=" ")
# indent() adds pretty-print whitespace in-place (Python 3.9+).
# tostring with xml_declaration=False omits the <?xml?> header.
indent(app_info, space=" ")
detection_xml = tostring(app_info, encoding="unicode", xml_declaration=False)
# --- Step 8: Build outer ZIP (STORED — no extra compression on encrypted content) ---
with zipfile.ZipFile(output_path, "w", compression=zipfile.ZIP_STORED) as outer: