IdeasOnCanvas / AppReceiptValidator

Parse and validate App Store receipt files
https://mindnode.com/opensource
Apache License 2.0
353 stars 41 forks source link

Sometimes macOS receipt validation fails, wrong MAC address used as device identifier #83

Open hannesoid opened 2 years ago

hannesoid commented 2 years ago

We have encountered at least one case where receipt validation failed on macOS for a user, but would have succeeded when using the primary MAC address retrieved the "old style" (before https://github.com/IdeasOnCanvas/AppReceiptValidator/pull/79).

hannesoid commented 2 years ago

Workaround (relevant on macOS only): If the receiptValidation fails with .incorrectHash, then try again using getLegacyPrimaryNetworkMACAddress as device identifier.

// validate
let result = AppReceiptValidator().validateReceipt(parameters: .default)
#if os(macOS)
if case .error(.incorrectHash, _, _) = result,
    // validate with fallback
    let fallbackParameters = self.makeCurrentReceiptValidationFallbackParameters() {
    return AppReceiptValidator().validateReceipt(parameters: fallbackParameters)
}
#endif
// return result
return result

// …

#if os(macOS)
/// Returns receipt validation parameters using the legacy style of retrieving the device identifier (MAC address)
func makeCurrentReceiptValidationFallbackParameters() -> AppReceiptValidator.Parameters? {
    guard let deviceIdentifierData = AppReceiptValidator.Parameters.DeviceIdentifier.getLegacyPrimaryNetworkMACAddress()?.data else { return nil }

    return .default.with { params in
        params.deviceIdentifier = .data(deviceIdentifierData)
    }
}
#endif
siracusa commented 1 year ago

@hannesoid: Is this workaround still needed? If so, will it eventually be incorporated into the package itself when this issue is closed?

hannesoid commented 1 year ago

Is this workaround still needed?

Good question! I can't answer it because we unfortunately have no data collection in place to establish if this workaround still gets active for recovering issues.

If so, will it eventually be incorporated into the package itself when this issue is closed?

With this uncertain situation it probably it would make sense to integrate it into the package in some way, or figure out why the primary mechanism for getting the MAC address fails to determine the correct one. If the issue is closed then it will be one of the two scenarios, either it will be because it's been integrated, or, because we somehow determined it's not needed or found a way to fix the primary issue (but I can't say we've been working on any of those tasks recently).

xhruso00 commented 9 months ago

I want to add some data points: Had a "hackintosh" user that did not have primary address (legacy mac address failed) and the new apple sample code returned en0 (non buildin) as the validation mac address. This failed during the validation as the correct mac address was en1 (non build in).

I think that the framework that picks mac address is AppleMediaServices.framework (+[AMSDevice macAddressData]). This call gets the mac address that matches the app receipt.

Upon closer inspection of internal apple implementation there is a difference to apple validation sample code. Internally apple cares about isBuiltIn. If isBuiltIn is true it will ignore wantBuiltIn. This is a logical difference to the sample code. I can confirm this behavior when using lldb + breakpoints and changing provided values.

Sample Code

if wantBuiltIn == CFBooleanGetValue(isBuiltIn) {
  return candidate
}

Internal Code

if CFBooleanGetValue(isBuiltIn) == true {
  return candidate
} else if wantBuiltIn == false {
  return candidate
} else { //interface is not build in but we want build in
  continue
}

Another difference is:

guard let service = io_service(named: "en0", wantBuiltIn: true)
            ?? io_service(named: "en1", wantBuiltIn: false) //instead of true
            ?? io_service(named: "en0", wantBuiltIn: false)

Due to the difference I would even rename wantBuiltIn to shouldFailIfNotBuiltIn

Last difference

//apple media services
guard IORegistryEntryGetParentEntry(service, "IOService", &controllerService) == KERN_SUCCESS else { continue }
let ref = IORegistryEntryCreateCFProperty(controllerService, "IOMACAddress" as CFString, kCFAllocatorDefault, 0)
guard let data = ref?.takeRetainedValue() as? Data else { continue }

//apple sample code
    if let cftype = IORegistryEntrySearchCFProperty(
        service,
        kIOServicePlane,
        "IOMACAddress" as CFString,
        kCFAllocatorDefault,
        IOOptionBits(kIORegistryIterateRecursively | kIORegistryIterateParents)) {
            return (cftype as! CFData)
    }

However I have no insights how these tweaks behaves on Macs with wired ethernet interfaces especially when Airplay 2 streaming content.

As of Sonoma a new C++ code is being present which means it will move to separate framework in near future

AMSCore::NetworkUtils::CopyInterfaceMACAddress(unsigned int)
AMSCore::NetworkUtils::GetFirstEthernetAddress(unsigned int, bool, std::__1::array<std::byte, 6ul>&)

Github search for GetFirstInterfaceMACAddress reveals that the same code is present in CommerceCore.framework -[ISPurchaseReceipt _parseTokens:] method which retrieves mac address to calculate receipt hash or it can be found in com.apple.CommerceKit.TransactionService.xpc -[StorePlatformOperation run]: ``GetKeybagHWInfo which handles purchases/receipt renewals.

Diagnostic Report:

Mac Address matching receipt: XXXXXXXX17C9A

Apple Sample Code Mac Address: XXXXXXXX7C99 en0, build-in(true): (null) en1, build-in(true): (null) en0, build-in(false): XXXXXXXX7C99

Apple Media Services Mac Address: XXXXXXXX17C9A

webdavfs mac address: 000000000000 directory mac address: ÙDP] (giberish for unknown reason) Legacy Mac Address: 000000000000

Reported interfaces: <__NSArrayM 0x600003558060>( <SCNetworkInterface 0x7fd9b4c22130 [0x7fff8004d2d0]> {type = Ethernet, entity_device = en4, entity_type = Ethernet, name(k) = "ether", address = REDACTED:4d:00, builtin = TRUE, path = IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/RP04@1C,3/IOPP/ARPT@0/itlwm/en4, entryID = 0x1000002f0, type = 6, unit = 4, order = 9 (Ethernet)}, <SCNetworkInterface 0x7fd9b4c3fff0 [0x7fff8004d2d0]> {type = Ethernet, entity_device = en0, entity_type = Ethernet, name(k) = "generic-ether"+"en0", address = REDACTED:7c:99, builtin = FALSE, path = IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/RP03@1C,2/IOPP/ethernet@0/RTL8111/en0, entryID = 0x1000002bc, type = 6, unit = 0, order = 9 (Ethernet)}, <SCNetworkInterface 0x7fd9b4c1c8a0 [0x7fff8004d2d0]> {type = Ethernet, entity_device = en1, entity_type = Ethernet, name(k) = "generic-ether"+"en1", address = REDACTED:7c:9a, builtin = FALSE, path = IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/RP05@1C,4/IOPP/ethernet@0/RTL8111/en1, entryID = 0x1000002bb, type = 6, unit = 1, order = 9 (Ethernet)}, <SCNetworkInterface 0x7fd9b4c1a180 [0x7fff8004d2d0]> {type = Ethernet, entity_device = en2, entity_type = Ethernet, name(k) = "bluetooth-pan-gn", address = REDACTED:4d:04, builtin = FALSE, order = 15 (BluetoothPAN_GN)} )

Note: itlwm -> is a Intel Wireless kext

/Library/Preferences/SystemConfiguration/NetworkInterfaces.plist interfaces:

{ Interfaces = ( { Active = 1; "BSD Name" = en0; IOBuiltin = 0; IOInterfaceNamePrefix = en; IOInterfaceType = 6; IOInterfaceUnit = 0; IOMACAddress = {length = 6, bytes = REDACTED7c99}; IOPathMatch = "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/RP03@1C,2/IOPP/ethernet@0/RTL8111/en0"; SCNetworkInterfaceInfo = { UserDefinedName = "Ethernet Adaptor (en0)"; }; SCNetworkInterfaceType = Ethernet; }, { Active = 1; "BSD Name" = en1; IOBuiltin = 0; IOInterfaceNamePrefix = en; IOInterfaceType = 6; IOInterfaceUnit = 1; IOMACAddress = {length = 6, bytes = REDACTED7c9a}; IOPathMatch = "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/RP05@1C,4/IOPP/ethernet@0/RTL8111/en1"; SCNetworkInterfaceInfo = { UserDefinedName = "Ethernet Adaptor (en1)"; }; SCNetworkInterfaceType = Ethernet; }, { "BSD Name" = en2; IOBuiltin = 0; IOInterfaceNamePrefix = en; IOInterfaceType = 6; IOInterfaceUnit = 2; IOMACAddress = {length = 6, bytes = REDACTED4d04}; IOPathMatch = "IOService:/IOResources/IOUserEthernetResource/IOUserEthernetResourceUserClient/IOUserEthernetController/en2"; SCNetworkInterfaceInfo = { UserDefinedName = "Bluetooth PAN"; }; SCNetworkInterfaceType = Ethernet; }, { "BSD Name" = en3; IOBuiltin = 0; IOInterfaceNamePrefix = en; IOInterfaceType = 6; IOInterfaceUnit = 3; IOMACAddress = {length = 6, bytes = REDACTED56e5}; IOPathMatch = "IOService:/IOResources/AppleUSBHostResources/AppleUSBLegacyRoot/AppleUSBXHCI@14000000/802.11n NIC@14500000/AppleUSBInterface@0/RtWlanU/en3"; SCNetworkInterfaceInfo = { UserDefinedName = "802.11n NIC"; idProduct = 33145; idVendor = 3034; kUSBProductString = "802.11n NIC"; }; SCNetworkInterfaceType = Ethernet; }, { Active = 1; "BSD Name" = en4; IOBuiltin = 1; IOInterfaceNamePrefix = en; IOInterfaceType = 6; IOInterfaceUnit = 4; IOMACAddress = {length = 6, bytes = REDACTED4d00}; IOPathMatch = "IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/RP04@1C,3/IOPP/ARPT@0/itlwm/en4"; SCNetworkInterfaceInfo = { UserDefinedName = Ethernet; }; SCNetworkInterfaceType = Ethernet; } ); "VERSION" = 20191120; }

Screenshot 2024-02-28 at 10 01 37
xhruso00 commented 8 months ago

Have successfully deployed this code to the Mac App Store:

    guard let service = io_service(named: "en0", wantBuiltIn: true)
            ?? io_service(named: "en1", wantBuiltIn: false)
            ?? io_service(named: "en0", wantBuiltIn: false)
        else { return nil }
    while candidate != IO_OBJECT_NULL {
        guard let cftype = IORegistryEntryCreateCFProperty(candidate,
                                                           "IOBuiltin" as CFString,
                                                           kCFAllocatorDefault,
                                                           0) else { return candidate }
        let isBuiltIn = cftype.takeRetainedValue() as! CFBoolean
        // if CFBooleanGetValue(isBuiltIn) || wantBuiltIn == false {
        if CFBooleanGetValue(isBuiltIn) == true {
            return candidate
        } else if wantBuiltIn == false {
            return candidate
        } else {
          //we are not build in and we want built in
        }

        IOObjectRelease(candidate)
        candidate = IOIteratorNext(iterator)
    }
hannesoid commented 8 months ago

@xhruso00 thanks for your thorough investigations. I've tried to apply your adjustments in https://github.com/IdeasOnCanvas/AppReceiptValidator/pull/100, please have a look.

I'm not sure how we would best proceed with merging that or not, but will discuss it.

xhruso00 commented 8 months ago

@hannesoid Nicely commented code! Not asking to merge. I wanted to created awareness that internal macOS code is different to the sample given to developers. Reported as FB13661140 "Sample code for obtaining mac address is different to internal implementation" under documentation incorrectness.

For years we used en0 (primary interface) to be the only correct one and it did indeed work. Even Apple's original validation code from 2019 [1],[4] only assumed en0. Minor problems popped up in 2019 and were related to Airplay 2 (introduced in 2018). As per [2], [4] IOBSDNameMatching(master_port, 0, "en0") created multiple iterations instead of one where the second one had NULL mac address. I assume that the issue with multiple iterations got addressed by Apple (updated sample code) to prioritize build-in interfaces.

This change makes the code to match internal implementation. The question is: What do we want? Do we want code correctness or code match?

The change might need to be tested with iMac Pro and Airplay 2 being actively used. Download app and see what mac address is matching. Alternatively mac mini might behave the same as the iMac Pro [2].

[1] https://web.archive.org/web/20190119090657/https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html [2] https://supermegaultragroovy.com/2019/10/22/check-your-receipt-validation-code/ [3] https://openradar.appspot.com/radar?id=5565290700079104 [4] https://gist.github.com/liscio/b40b16feb1a0ff492980bc800a158668#file-copy_mac_address-m-L26

xhruso00 commented 8 months ago

Just looked at the CommerceCore.framework on 10.12 Sierra (System->PrivateFrameworks->CommerceKit.frame->Frameworks->CommerceCore.framework) and it confirms my research. In the past Apple looked for interface that has matched @'IOPrimaryInterface" + IOServiceMatching (kIOEthernetInterfaceClass). This explains first comment on this issue. If anyone has a deployment target of 10.12 and lower -> he must use availability macro and use the legacy way of obtaining mac address. The last release of High Sierra 10.13 uses the new way of obtaining mac address.

Screenshot 2024-03-11 at 13 50 22