Attaching a software license agreement to a signed disk image in Big Sur breaks the disk image file

Big Sur finally retired the (depreciated) hdiutil flatten and hdiutil unflatten commands.

In its place is the hdiutil udifrez which writes legacy resources to a UDIF disk image. However, no matter what I've tried, it breaks our distribution disk images.

Specifically, what our release scripts used to do was this (simplified version):
  • This process starts with the application to release (PRODUCT_CLIENT_APP) and an existing Disk Image file (PROTO_IMAGE_SRC) that's all pretty (has a custom background, window size, ...).

  • The prototype disk image is duplicated, mounted, and the release app is copied onto it:

Code Block
cp -f "${PROTO_IMAGE_SRC}" "${PROTO_IMAGE_WORK}"
hdiutil attach "${PROTO_IMAGE_WORK}"
cp -R "${PRODUCT_CLIENT_APP}" "${PROTO_VOLUME_DIR}"
  • The image is then unmounted and compressed

Code Block
hdiutil detach "${PROTO_DEVICE}"
hdiutil convert "${PROTO_IMAGE_WORK}" -format UDCO -o "${DMG_RELEASE_SIGNED}"
  • Here's where the license was attached. The disk image was converted to a forked disk image file, the resource file was compiled and attached, and then converted back to a flat disk image file.

Code Block
hdiutil unflatten "${DMG_RELEASE_SIGNED}"
Rez -a "${BUILD_TOOLS_DIR}/software_license.r" -o "${DMG_RELEASE_SIGNED}"
hdiutil flatten "${DMG_RELEASE_SIGNED}"
  • The completed disk image is then signed and uploaded for notarization:

Code Block
codesign --sign "${DMG_SIGN_IDENTITY}" --verbose=3 "${DMG_RELEASE_SIGNED}"
xcrun altool --notarize-app --primary-bundle-id "${PRIMARY_BUNDLE_IDENTIFIER}" -f "${DMG_RELEASE_SIGNED}" ...
  • After noterization is complete, the signed image is stapled and verified:

Code Block
cp "${DMG_RELEASE_SIGNED}" "${DMG_RELEASE_FINAL}"
stapler staple --verbose "${DMG_RELEASE_FINAL}"
spctl --assess --type open --context context:primary-signature --verbose=2 "${DMG_RELEASE_FINAL}"


This all used to work just fine, until the flatten and unflatten commands went away.

Since the hdiutil udifrez -rsrcfork option is either unimplemted or broken, we tried using the hdiutil udifrez -xml option. Since the XML file for this command isn't documented anywhere, we created one using the udifderez command using the license resources from a previous release.

Armed with this XML file, I replaced the (unflatten/Rez/flatten) license attachment step with this:
Code Block
hdiutil udifrez -xml "${BUILD_TOOLS_DIR}/software_license.xml" '' "${DMG_RELEASE_SIGNED}" 

(note the extra, blank, argument because there's apparently another bug—or documentation failure—in udifrez, and if omitted the command fails)

Everything proceeds swimmingly, through the signing and stapling, except the resulting file is broken. When you open the disk image the SLA appears. Agree to the license, you're presented with a failure dialog:

Code Block text
Warning
The following disk images couldn't be opened
Image Reason
release_2.2.3.dmg image data corrupt

If I leave out the single hdiutil udifrez -xml ... step, everything works (except that there's no attached license agreement).

I can't attach the license before the disk image is compressed, because it's the wrong format.

I can't attach the license after the disk image is signed or stapled, for obvious reasons.

So we're stuck. I can't find any way of attaching a SLA to a signed and notarized distribution disk image, using the current set of tools, without breaking the disk image.
Answered by peterguy in 652142022
I was faced with the same problem, and solved it, thanks in a large part to your wonderfully-detailed post.

TL;DR: remove the array identified by the key "blkx" from the extracted xml file before using it.

Read on for more details...

I examined the extracted xml file - which is a plist - and noticed that there are several sections to it, each section identified by a key. If it helps, the XPath to the section key is /plist/dict/key. There are other keys in the structure, but they are all nested deeper in arrays and dictionaries (I am quite ignorant of plists; there is probably a much better way to do this)
These are the sections I found, in order:
  • <key>LPic</key>

  • <key>STR#</key>

  • <key>TEXT</key>

  • <key>TMPL</key>

  • <key>blkx</key>

  • <key>styl</key>

I noticed that while most of the sections have basically textual data, like button and EULA text, the "blkx" section contains what look like binary data, with names like, "Protective Master Boot Record (MBR : 0)", and "disk image (Apple_HFS : 4)". I figured those were specific to the image, so I removed that whole "blkx" section, including the key.

When I used that modified plist as input for hdiutil udifrez -xml ..., the resulting image worked fine: displayed the EULA and opened as expected.

Note that based on what I have read elsewhere online, the "TEXT" section may be "RTF" or something else, depending on the format of the EULA. Yours is named with a ".r" extension; I'm not sure what format that would be, but you should be able to find the correct section for it.

You should be able to continue using your EULA file, instead of relying on updating the plist every time the EULA changes, instead keeping the plist as a template, and inserting the EULA into it as a build step. The EULA needs to be base64-encoded; I was careful to slavishly copy the format in the extracted plist, so I wrapped mine at 52 characters and indented it with three tabs characters.

My template looks like this (truncated for brevity):
Code Block
...
<key>TEXT</key>
<array>
<dict>
<key>Attributes</key>
<string>0x0000</string>
<key>Data</key>
<data>
${ENGLISH_SLA}
</data>
<key>ID</key>
<string>5000</string>
<key>Name</key>
<string>English SLA</string>
</dict>
</array>
...

I save it as my-eula.plist.template.
Note, again, that you may need to use a section other than "TEXT", depending on the format of your EULA.

And I use this process to insert the EULA into it:
Code Block
# base64-encode the English-language EULA, breaking at character 52 because that's how it was done in the plist extracted by udifderez
# And add three tab characters to the beginning of every line, slavishly following the format of the extracted plist
ENGLISH_SLA="$(base64 -b 52 my-eula.txt | sed s$'/^\(.*\)$/\t\t\t\\1/')"
# this is one way of doing token replacement, when the tokens are formatted as shell interpreter variables
eval "cat >my-eula.plist <<EOF
$(<my-eula.plist.template)
EOF
"
# add the eula to the dmg
# note that udifrez requires an argument after the plist and before any other documented argument.
# I am using a blank argument, but behavior appears to be the same no matter what value is there
hdiutil udifrez -xml my-eula.plist '' -quiet "${appname}-${appversion}.dmg" || {
echo "failed to add the license to the image" 1>&2
exit 1
}


Apple's getting more aggressive with deprecations. Used to, they would wait several years after officially deprecating something before removing it. But it turned out that 3rd party developers never paid attention to that and that long lead time was a waste of time. The flatten and unflatten commands were only deprecated in Catalina. The moral of the story, "deprecated" means "fix this now".

My suggestion is to change your app so that it displays the SLA on first launch. That's easy enough to do and then its done. Your embedded SLA problem goes away. Perhaps even the rationale for having a DMG in the first place goes away too. You only need a DMG if you need to install multiple system modifications as root. With the SLA embedded in your app, you can just zip up the app and distribute the zip file. There are companies that will extract your app from the DMG and repost it on their site, thereby stripping your SLA. In short, it was never a good idea to use a DMG if you didn't have to.
Accepted Answer
I was faced with the same problem, and solved it, thanks in a large part to your wonderfully-detailed post.

TL;DR: remove the array identified by the key "blkx" from the extracted xml file before using it.

Read on for more details...

I examined the extracted xml file - which is a plist - and noticed that there are several sections to it, each section identified by a key. If it helps, the XPath to the section key is /plist/dict/key. There are other keys in the structure, but they are all nested deeper in arrays and dictionaries (I am quite ignorant of plists; there is probably a much better way to do this)
These are the sections I found, in order:
  • <key>LPic</key>

  • <key>STR#</key>

  • <key>TEXT</key>

  • <key>TMPL</key>

  • <key>blkx</key>

  • <key>styl</key>

I noticed that while most of the sections have basically textual data, like button and EULA text, the "blkx" section contains what look like binary data, with names like, "Protective Master Boot Record (MBR : 0)", and "disk image (Apple_HFS : 4)". I figured those were specific to the image, so I removed that whole "blkx" section, including the key.

When I used that modified plist as input for hdiutil udifrez -xml ..., the resulting image worked fine: displayed the EULA and opened as expected.

Note that based on what I have read elsewhere online, the "TEXT" section may be "RTF" or something else, depending on the format of the EULA. Yours is named with a ".r" extension; I'm not sure what format that would be, but you should be able to find the correct section for it.

You should be able to continue using your EULA file, instead of relying on updating the plist every time the EULA changes, instead keeping the plist as a template, and inserting the EULA into it as a build step. The EULA needs to be base64-encoded; I was careful to slavishly copy the format in the extracted plist, so I wrapped mine at 52 characters and indented it with three tabs characters.

My template looks like this (truncated for brevity):
Code Block
...
<key>TEXT</key>
<array>
<dict>
<key>Attributes</key>
<string>0x0000</string>
<key>Data</key>
<data>
${ENGLISH_SLA}
</data>
<key>ID</key>
<string>5000</string>
<key>Name</key>
<string>English SLA</string>
</dict>
</array>
...

I save it as my-eula.plist.template.
Note, again, that you may need to use a section other than "TEXT", depending on the format of your EULA.

And I use this process to insert the EULA into it:
Code Block
# base64-encode the English-language EULA, breaking at character 52 because that's how it was done in the plist extracted by udifderez
# And add three tab characters to the beginning of every line, slavishly following the format of the extracted plist
ENGLISH_SLA="$(base64 -b 52 my-eula.txt | sed s$'/^\(.*\)$/\t\t\t\\1/')"
# this is one way of doing token replacement, when the tokens are formatted as shell interpreter variables
eval "cat >my-eula.plist <<EOF
$(<my-eula.plist.template)
EOF
"
# add the eula to the dmg
# note that udifrez requires an argument after the plist and before any other documented argument.
# I am using a blank argument, but behavior appears to be the same no matter what value is there
hdiutil udifrez -xml my-eula.plist '' -quiet "${appname}-${appversion}.dmg" || {
echo "failed to add the license to the image" 1>&2
exit 1
}


Thanks to both the OP and @peterguy. Worked like a charm.
I've found this thread while looking for a solution to the deprecated "flatten/unflatten" arguments. Thanks, great info!

However, I can't figure out what to do if my plain text (not RTF!) SLA text contains non-ASCII characters. Suppose I want to add, say, the Greek Omega letter to my text. I tried to base64-encode Unicode strings, but when I mount the resulting DMG, the SLA text displays gibberish in place of non-Latin chars.

As the input to the base64 encoding function, I've tried:

UTF8
UTF8 with a 3-byte BOM
UTF16
UTF16 with a 2-byte BOM

None of the above works. Is there a way to do what I want, or the whole thing doesn't even support Unicode?


Hi,

Please can you publish a complete simple test example - to add EULA to a DMG?

I tried to use example script from above. But I can't find full my-eula.plist.template and my-eula.plist files.

Thanks in advance!
Thanks @dawn2dusk & @peterguy. Just implemented this in the create-dmg script:

https://github.com/create-dmg/create-dmg/commit/f75cc032d77672adab60cebd49db000a8ff7446a
Attaching a software license agreement to a signed disk image in Big Sur breaks the disk image file
 
 
Q