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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user