Wrapping iOS apps with the Intune App Wrapping Tool: a practical guide

How to wrap an iOS app for Intune without touching its code: prerequisites, signing, MSAL settings, which app protection policies a wrapped app supports, and the three failures that cost me the most time.

At Nimway we needed a version of the app that enterprise customers could manage with Microsoft Intune: their own PIN rules, their own limits on copy and paste, their own wipe. The app had no Intune code in it, and the fastest way to get there was the Intune App Wrapping Tool for iOS. You give it a signed app, a provisioning profile and a signing certificate. It gives back a new .ipa with Intune’s app protection layer built in, signed and ready to upload.

Microsoft’s documentation covers the happy path well. This post is the guide I wanted at the time: what wrapping can and can’t do, what you need before you start, how to run the tool, and the three things that broke for me and aren’t obvious from the docs.

Wrapping tool or Intune App SDK?

Intune has two ways to make an iOS app respond to app protection policies:

  • The App Wrapping Tool works on a finished app, with no source code. Microsoft positions it for internal line-of-business apps.
  • The Intune App SDK is a framework you integrate into the app’s code. It’s meant for apps in the App Store, but any app can use it.

The wrapping tool doesn’t work on App Store apps at all: it needs an unencrypted app, signed by your company or by the vendor who built it for you. An app downloaded from the App Store is encrypted, so wrapping is only an option for apps you distribute yourself.

Most policy settings work the same way in a wrapped app as with the SDK: PIN or Face ID to open the app, limits on sending and receiving data, cut, copy and paste restrictions, encrypted app data, blocking jailbroken devices, offline grace period, full wipe of app data, minimum app and OS versions. A few need code in the app and are SDK only:

FeatureSDKWrapping tool
Prevent “Save as”yesno
App configuration through the MAM channelyesno
Multi-identity (work and personal accounts in one app)yesno
Selective wipe of work data in multi-identity appsyesno
Conditional Access “Require app protection policy”yesno
Mobile Threat Defense device risk levelyesno
Customizable styleyesno

If your customers need any of these, wrapping is the wrong choice and you should budget for the SDK. If the app is internal, gets released a few times a year and needs PIN, data transfer and copy-paste rules, wrapping gets you there without changing the code.

What you need before you start

From the prerequisites in Microsoft’s documentation:

  • A Mac with Xcode 16 or later. The tool is a macOS command-line app.
  • The right version of the tool. Releases are tied to the Xcode version the app was built with. At the time of writing (October 2026), the 22.x releases are for apps compiled with Xcode 27 and the 21.x releases are for Xcode 26. Check the release notes on GitHub before every wrap.
  • The input app: an .ipa or .app, signed, built for iOS 16 or later, not encrypted, with no extended file attributes. Its entitlements must already be set.
  • Signing for distribution: an Apple Developer Enterprise Program account, an In-House distribution certificate and an In-House provisioning profile. For internal testing, a development certificate works too, as long as the profile includes the test devices.
  • An app registration in Microsoft Entra ID. Wrapped apps sign in with MSAL, so the tool needs your app’s client ID and redirect URI.

Register the app in Entra ID

  1. Register the app in Microsoft Entra ID and note the Application (client) ID.
  2. Add a redirect URI for iOS in the MSAL format: msauth.<bundle id>://auth.
  3. Give the app access to the Intune MAM service: in the app registration, add a permission from APIs my organization uses, find Microsoft Mobile Application Management and select the delegated permission DeviceManagementManagedApps.ReadWrite.

The client ID (-ac) and redirect URI (-ar) are required, and each app needs its own pair. The authority (-aa, for example https://login.microsoftonline.com/<tenant ID>/) is only required for single-tenant apps.

If you’re updating old scripts: older guides and scripts use ADAL settings. ADAL reached end of support in 2023, and the current tool only takes the MSAL ones: MSAL Client ID, MSAL Reply URI and MSAL Authority in the plist, or -ac, -ar and -aa on the command line.

Running the tool

The tool ships as a disk image from Microsoft’s GitHub repository. Mount it and you get IntuneMAMPackager. You can pass everything as flags, but a plist file is easier to keep in version control and to run from CI. The template is Parameters.plist next to the binary:

Xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Input Application Package Path</key>
    <string>/path/to/MyApp.ipa</string>
    <key>Output Application Package Path</key>
    <string>/path/to/MyApp_Wrapped.ipa</string>
    <key>Provisioning Profile Path</key>
    <string>/path/to/MyApp_InHouse.mobileprovision</string>
    <key>SHA-1 Certificate Hash</key>
    <string>12A3BC45D67EF8901A2B3CDEF4ABC5D6E7890FAB</string>
    <key>MSAL Client ID</key>
    <string>xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</string>
    <key>MSAL Reply URI</key>
    <string>msauth.com.example.myapp://auth</string>
    <key>Extension Provisioning Profile Paths</key>
    <array>
        <string>/path/to/MyAppWidget_InHouse.mobileprovision</string>
    </array>
    <key>Verbose Enabled</key>
    <true/>
</dict>
</plist>
Shell
./IntuneMAMPackager/Contents/MacOS/IntuneMAMPackager -f Parameters.plist

The same run with flags:

Shell
./IntuneMAMPackager/Contents/MacOS/IntuneMAMPackager \
  -i ~/Desktop/MyApp.ipa -o ~/Desktop/MyApp_Wrapped.ipa \
  -p ~/Desktop/MyApp_InHouse.mobileprovision \
  -c "12A3BC45D67EF8901A2B3CDEF4ABC5D6E7890FAB" \
  -ac "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -ar "msauth.com.example.myapp://auth" \
  -x ~/Desktop/MyAppWidget_InHouse.mobileprovision \
  -v

Always turn on verbose output (-v, or Verbose Enabled in the plist). Without it, the errors are often too short to tell what went wrong. On success the tool prints “The application was successfully wrapped”.

Two more flags worth knowing:

  • -b sets the bundle version (CFBundleVersion) of the wrapped app. By default the tool changes it; -b without a value keeps the original, which Microsoft doesn’t recommend.
  • -e removes entitlements that the provisioning profile doesn’t have. It makes the wrap succeed, but the features behind those entitlements stop working. Use it for a vendor app that asks for more than it needs, not to silence an error in your own app.

Three things that broke

Settings.bundle with a malformed Root.plist

The first run failed with:

Text
Failed to extract Root.plist from the app's settings bundle.
Found non-key inside <dict> at line 7
Malformed data byte group at line 1; invalid hex

A wrapped app gets a Microsoft Intune section in its page in the Settings app (that’s where the diagnostics console lives), so the tool reads the app’s Settings.bundle. Our Root.plist had a PreferenceSpecifiers key with no value after it:

Xml
<!-- broken: the key has no value -->
<dict>
    <key>PreferenceSpecifiers</key>
    <key>StringsTable</key>
    <string>Root</string>
</dict>

iOS shrugged that off; the wrapping tool didn’t. An empty array is enough:

Xml
<!-- fixed -->
<dict>
    <key>PreferenceSpecifiers</key>
    <array/>
    <key>StringsTable</key>
    <string>Root</string>
</dict>

Check the plist in the exported app before you wrap it:

Shell
unzip -p MyApp.ipa "Payload/MyApp.app/Settings.bundle/Root.plist" | plutil -lint -

While you’re there, make sure Settings.bundle belongs to the main app target only, not to any extension. It’s easy to tick the wrong box when you add a target.

Every extension needs its own provisioning profile

The tool re-signs every extension in the app (widgets, share extensions, notification service extensions), so each one needs a provisioning profile for its own bundle ID. If one is missing, the wrap stops:

Text
There are more extensions than specified provisioning profiles.
Each extension must have a corresponding provisioning profile.

To see which extensions the app has and which entitlements each of them needs:

Shell
./IntuneMAMPackager/Contents/MacOS/IntuneMAMPackager -xe -i MyApp.ipa

Then add one profile per extension to Extension Provisioning Profile Paths (or -x). The entitlements in each profile have to match what the extension uses: app groups, keychain sharing and push notifications are the usual suspects. To compare them, look at what the app is signed with and what the profile allows:

Shell
codesign -d --entitlements :- "Payload/MyApp.app"
security cms -D -i "Payload/MyApp.app/embedded.mobileprovision"

Watch the expiry dates. The tool doesn’t check whether a profile or a certificate has expired. It wraps and signs the app anyway, and you find out only when the app refuses to install on a device.

The signing certificate

The tool identifies the signing certificate by its SHA-1 hash (-c, or SHA-1 Certificate Hash in the plist). Old scripts and guides that pass a certificate name don’t work. To list the hashes of the signing identities in your keychain:

Shell
security find-identity -v -p codesigning

Each line starts with the 40-character hash, followed by the certificate name. You can also copy it in Keychain Access: open the certificate, Get Info, and look under Fingerprints.

What cost me time was having the same certificate in the keychain more than once, which happens easily after renewals and re-imports. The tool then fails with “An invalid signing certificate was specified”, even though the certificate is fine. Delete the duplicates, keep one copy with its private key, and run again. If the certificate and profile can sign the app in Xcode, they’re good enough for the tool.

Deploying the wrapped app and its policies

  1. Upload the app. In the Intune admin center, add it under Apps as an iOS/iPadOS line-of-business app and upload the wrapped .ipa. When you update an app that’s already there and the upload fails, Microsoft’s advice is to add it as a new app and delete the old one.
  2. Create an app protection policy. Create an iOS/iPadOS app protection policy and, in the app selection, add your app under custom apps by its bundle ID. A custom app can’t be combined with “all public apps” in the same policy. Assign the policy to the user groups that should get it.
  3. Start with relaxed settings. For the first test, use a long PIN timeout, allow Face ID and leave copy and paste open. Tighten the settings once the basic flow works: debugging policy behaviour and integration problems at the same time is no fun.
  4. Test on a device. Install the app and open it. It should ask for a work account sign-in and then apply the policy, for example by asking you to set a PIN. If it opens like the unwrapped app, the wrap didn’t take effect.

A few behaviours surprise testers:

  • Encryption of app data only works when the device has a passcode.
  • By default, a wrapped app asks for the work account again when it’s relaunched.
  • File upload dialogs can be used to get around copy and paste restrictions, for example by uploading a screenshot. Microsoft lists this as a known limitation.
  • When something goes wrong on a device, open the app’s page in the Settings app, then Microsoft Intune, and turn on Display Diagnostics Console. The next launch lets you collect and share the Intune logs.

Rewrapping

You don’t wrap once. You wrap again:

  • For every release of your app. Always wrap the original build: the tool refuses an app it has already wrapped.
  • When Microsoft releases a new version of the tool. That happens every 6–8 weeks, with bug fixes and support for new policy settings and new iOS versions. A new Xcode version usually means a new major version of the tool.

You can wrap with a different certificate or profile than the one the app was originally signed with, but the new profile must have all the entitlements the app uses.

What I’d do differently

I’d put the wrap in the release pipeline from the first day instead of running it by hand. The inputs never change between runs: the app, the profiles, the certificate hash, the client ID and the redirect URI. Scripting them removes the whole class of mistakes that come from picking the wrong profile or the certificate you forgot to renew. Pin the tool version in the same script and update it on purpose, after reading the release notes.

And I’d keep a calendar reminder for certificate and profile expiry. The tool won’t warn you, and the first sign of trouble is a release that installs on nobody’s device.

Sources