Developer PSA: Check Your Receipt Validation Code
• Chris Liscio
• Chris Liscio
There is a bug in macOS that triggers a nasty failure in the receipt validation code that you probably ship with your Mac app right now, and you need to fix it.
Mac apps that are sold on the App Store include code that verifies that the app was obtained legitimately from Apple. Part of this verification process ensures that the receipt was generated specifically for the Mac that is currently running the software.
This code requires a unique identifier from the machine, which—on the Mac—happens to be the MAC address of the primary network interface , en0
. Apple helpfully provides developers with sample code to obtain this. I've reproduced the code in a gist so that I can reference it line-by-line as I describe the failure.
While playing audio using AirPlay 2, there are two devices that have a BSD Name
equal to en0
. Both the actual en0
interface, and a new device called IOTimeSyncEthernetModernInterfaceAdapter
(or IOTimeSyncWiFiInterfaceAdapter
, depending on your Mac) that is only present while AirPlay is active.
If you want to see this in action for yourself, type the following command at the Terminal:
ioreg | grep -C10 en0
Then, start some audio playing over AirPlay 2, and repeat the above command. You'll notice that the tree underneath en0
has expanded to include a new "time sync" device.
Now, type the following command (substituting IOTimeSyncWiFiInterfaceAdapter
if your Mac only has built-in Wi-Fi):
ioreg -r -d 1 -n IOTimeSyncEthernetModernInterfaceAdapter
And notice that the output contains the following entry:
"BSD Name" = "en0"
Well that's quite unexpected, wouldn't you agree? I filed FB7369072 with Apple to track this.
In the presence of the bug above, the matchingDict
created on line 20 will result in two iterations of the loop on line 32.
On the first iteration of the while
loop, the MAC address is obtained successfully.
However, on the second iteration, line 38 clears out the MAC address, and line 40 returns a NULL
CFDataRef
!
When copy_mac_address
returns NULL
, your receipt validation code fails, and probably results in your application calling exit(173)
to ask the App Store for a new receipt. Unfortunately, a new receipt isn’t going to help, and you get stuck in a very strange loop.
For me, this resulted in the app repeatedly appearing & disappearing in the Dock for quite some time. Eventually, the system finally gave up and told me the app was “Damaged” and needed to be re-downloaded.
As far as I can tell, the only end-user workaround requires that AirPlay playback is not running when launching apps that are affected by this bug.
In some cases, the bug will persist after AirPlay is stopped, and a restart may be required to clear this up. This is the state that I found myself in when I first discovered the issue.
The quick fix is to change the while
loop on line 32 to an if
statement, and skip the 2nd go-around in the loop. But I don't like this approach at all, and don't recommend it.
Instead, I decided to go with the same approach that Apple uses themselves in a few spots:
GetMACAddress
in DirectoryServiceGetPrimaryMACAddress
and FindPrimaryEthernetInterfaces
in webdavfsThe keys to this alternate approach are as follows:
kIOEthernetInterfaceClass
, which only matches network interfaces. (Before you ask—yes, this includes Wi-Fi adapters.)kIOPrimaryInterface
is set to true, which matches the built-in, primary network interface.I also don’t bother with a while
loop on the returned iterator for primary interfaces. The code I ended up with looks most similar to the DirectoryService
code linked above.
I don’t think it’s very common at all, which is probably why this bug slipped through the cracks this long. Also, it requires that someone is streaming something over AirPlay.
In addition, it appears that this only affects users that use wired built-in network adapters. In my case, I use the wired ethernet adapter in my iMac Pro. I don’t know whether merely having the wired Ethernet adapter causes the issue, or whether you have to use the wired adapter to stream to an AirPlay device. An exercise left up to the reader, I guess.
At any rate, I have verified on my MacBook Pro that the consequences of the bug are not as bad. While the strange "time sync" device still appears with a BSD Name of en0
, and the while
loop is executed twice, the parent device (which is en0
itself) unexpectedly carries a (correct!) value for the MAC address.
So the “weird behavior” (a second, unexpected en0
entry) still persists, but the correct MAC address is not overwritten during the second iteration of the while
loop.
First and foremost, the consequences are horrible. A customer that downloaded your software—in some cases having just paid for it—will be unable to launch your app while their Mac is in this state.
It won't crash on them, no. Instead, the system tells your customer that your application is damaged! They'll re-download it as instructed, and nothing will change the outcome. At this point, your customer is fed up. They'll demand a refund, and tell everyone they know that your software is junk.
Sadly, their sentiment is not entirely incorrect. Put yourself in their shoes, and you'd feel exactly the same way—especially if money changed hands.
Now, the second reason you should care is that the bug could become more widespread. While the Wi-Fi-only Macs currently obtain the MAC address successfully, it's a complete fluke! The original code asks the parent of the en0
interface for the MAC address, because the en0
device is not expected to carry a value for the MAC address.
It's a total fluke that Apple's sample code still works on these Macs, and so this behavior could change on upcoming hardware or software updates, and cause the issue to appear more frequently in the future. I don't know about you, but I wouldn't risk it.
The third and final reason? It affects me! I listen to music from my Mac over AirPlay frequently, and it pisses me off that a few of the apps that I purchased from the App Store refuse to launch until I disconnect from AirPlay.
If—like me—you’re not fully convinced about kIOPrimaryInterface
being singular, or the use of a while
loop, you can read Apple’s code for yourself. Search for isPrimaryInterface
to see how the kIOPrimaryInterface
property is set. It’s assigned to the built-in device with a unit number equal to 0. Or, to put it another way, en0
.
You may also notice that the DirectoryService code doesn’t bother with a while
loop, but the webdavfs code does. However, despite the pluralized FindPrimaryEthernetInterfaces
function name in the latter example, the comments seem to indicate that they expect only a single device to be returned.
Given the combination of querying only the kIOEthernetInterfaceClass
devices, and the kIOPrimaryInterface
flag, you'll only ever get exactly the device you're interested in.
Any half-decent developer is justified in feeling some worry about making changes to their receipt validation code. I definitely did for a short while.
Apple tells us to “use this exact same code” to get the Mac’s device ID. If we implement our device ID code differently from Apple, and a weird bug pops up, then our receipt validation should be equally as broken as theirs! So everything should just match up!
Fortunately, there are a few factors working against that argument:
en0
can be mis-applied to another device—one that doesn’t even represent a network interface!—is a pretty clear indication that Apple’s documented method is fragile.Yes. I have received verification from a very small handful of developers (shipping quite-popular apps) that the same mechanism is working for them in the field.
As of a few days ago, Capo shipped using this alternate approach as well.
Maybe go download it, check it out, and let me know if you have trouble launching it?