In my last post, I showed how the latest Apple TV system checks for an Apple-signed certificate before allowing changes to certain device settings. In particular, this prevents easily enabling the “Add Site” application, detailed in my 2013 DerbyCon talk. However, as I mentioned in the last post, it’s possible to load the profile on an Apple TV running 5.2 or 5.3, and then upgrade to 6.0, and retain access to Add Site. The problem then is that the system won’t actually permit adding any sites. What gives?

When adding a site (or channel or application or whatever you want to call it), the system first asks for a URL which points to a “vendor bag,” a .plist file defining the new application. Then it prompts for a site name, and then finally exits with the error “The site could not be verified for this device. Please check logs and retry.” Pulling the AppleTV binary into IDA Pro, we eventually find where the series of Add Site prompts occurs, in the method “[MEInternetTextEntryDialog _showNextPrompt]”.

This method is basically a 4-element finite state machine. When in state 0, it calls a method which prompts the user to enter the URL, and then changes the state to 1. In state 1, it asks for the new site’s name, then goes to state 2. In state 2, it sets up a call to “_verifySiteInfo”, then in state 3, it checks the result of that verification. If the response is good, it adds the site. If not, it shows the error and the user goes back to the beginning.

So what’s in “_verifySiteInfo”? That calls “[ATVAddSiteEntry entryWithName: andURL:]”, which calls “sub_186700”, which then calls “[ATVVendorBag isTrusted]”. If the response to the isTrusted call is zero, then the next pass through “_showNextPrompt” (in state 3) will display the error message and return to step 0.

So the actual check happens in the “[ATVVendorBag isTrusted]” method. Here’s the bulk of that routine, as disassembled by IDA Pro (and re-written manually to make it easier to follow):

  result = 1;

//
// If /AppleInternal/Library/PreferenceBundles/Carrier Settings.bundle
//  exists, it's an internal build, and allow the site addition
//
  if ( [[ATVSettingsFacade sharedInstance] runningAnInternalBuild])
    return result;


//
// If the bag doesn't include icloud-auth-enabled, skip to next check
//
  if (! [[self valueForKey:"icloud-auth-enabled"] boolValue])
  {
    result = 1;
    goto LABEL_1;
  }


//
// The bag includes icloud-auth-enabled. Get and verify signature.
//
  sig = [self valueForKey:"iCloudAuthSignature"];
  text = [self merchantID];
  text = [text stringByAppendingString:"iCloudAuth"];
// text will now be "<merchant id>iCloudAuth"

  text_utf8 = [text UTF8String];
  text_len = strlen(text_utf8);

// put the text to be signed into a byte array
// and put the signature we pulled from the bag into an list of signatures
  text_bytes = [NSData dataWithBytes:text_utf8 length:text_len];
  sig_array = [NSArray arrayWithObjects:sig count:1];

// now we take the text, and the list of signatures, and see if a sig matches
  result = sub_43C5D0(text, sig_array);  // Here's where the fun happens 

  if ( result == 1 ) // It passed the test -- the signature is valid
  {
LABEL_1:

    if ( ! [[self valueForKey:"vendorBagLoadedByAddSite"] boolValue])
      return result;

      // return 1 if:
      //  * not added by Add Site AND
      //    * not icloud-auth-enabled or
      //    * is icloud-auth-enabled and signature matches


// If we got here, then vendor bag was loaded by addSite
// So we have to see if device is authorized

    text = [self merchantID];
    text = [text stringByAppendingString: [ATVDevice uniqueID]];
// Now text is "<merchant id><device udid>"

// And we do the same stuff as before with UTF8 strings, etc.
    text_utf8 = [text UTF8String];
    text_len = strlen(text_utf8);
    text_bytes = [NSData dataWithBytes:text_utf8 length:text_len];

// Only this time the signatures are stored in the
// com.apple.frontrow settings, likely loaded onto the device
// via the profile that enabled Add Site to begin with.
// And we may have more than one authorization (signature) to check.
    sig_array = [ATVSettingsFacade addSiteDeviceAuthorizations];

    result = sub_43C5D0(text, sig_array);  // test against all the signatures
    goto LABEL_2;
  }
// if we got here, then iCloudAuthSignature failed
  result = 0;

LABEL_2:
  if ( !result && _internalLogLevel >= 3 )
  {
    _ATVLog(3, [self merchantID], @"Trust failure for merchant %@: %@");
    result = 0;
  }
  return result;

This is all sort of complicated. Summarized, in pseudo-code:

If runningAnInternalBuild: Trusted

If icloud-auth-enabled:
  If iCloudAuthSignature invalid: Not Trusted

If vendorBagLoadedByAddSite: 
  If device is authorized: Trusted
  Else: Not Trusted
Else: Trusted

Basically, if the bag has an icloud-auth-signature, it better be valid, and if the bag was loaded by Add Site, then it the device has to be authorized for this particular merchant.

So, what is this elusive signature? We can find examples of icloud-auth-signature in the StoreFront call, mentioned in the last post:

<key>merchant</key>
<string>iMovieNewAuth</string>

<key>icloud-auth-enabled</key>
<true/>

<key>icloud-auth-signature</key>
<data>YxH4I6zha8O331odzY3Zf+APR9gYi/Atorp84x3BTqVg5N4EqAwzyh72UpiF4mgCw5CLneC/I/VlNUntZB17y6yXLstZpbRvKnr/LoQtccmLo7ELWcmFWfU3gEb7u4ne/E1N92oCHrOIxsBbnEqkOp65M47k9x6GojqDsfT4Lrr0XIJ86LH+cl2UIgVQlR77Q8fnSnvChLqGjwIdvKEi2xcfm/v40bFN0JkRV1wrEsw8Zvu3m53GKEOsLbHVCd6Waqsisopbsk3Q4j+D50EnJ699n4UlNoat0bEc4Jz8TjEHoMnB5f23NV0KlFOoC0LPVdJecbAH0bGjfD9WjgMHhA==</data>

This is the same format as similar signatures for javascript-url-signature and root-url-signature, and it validates in exactly the same way,with the same key. Interestingly, though, it doesn’t look like javascript-url and root-url signatures are actually checked in version 6.0! (Though it’s possible I made a mistake on that – I could find the checks in 5.2, but not in 6.0). The validation happens in the code above, at sub_43C5D0. This routine, paraphrased again, looks like this:

hash = CC_SHA1(text, len(text)
key = SecKeyCreateRSAPublicKey(0, 13208544, 270, 1)
  
for sig in signatures:
    if  (! SecKeyRawVerify(key, 32770, hash, 20, sig, len(sig))):
        return 1

return 0

The 32770 above (in hex, 0x8002) is a constant that tells SecKeyRawVerify to expect a PKCS1SHA1 signature, which we can find in SecKey.h:

/* For SecKeyRawSign/SecKeyRawVerify only, data to be signed is a SHA1
   hash; standard ASN.1 padding will be done, as well as PKCS1 padding
   of the underlying RSA operation. */
kSecPaddingPKCS1SHA1 = 0x8002,

Going to address 13208544, or 0xc98be0 in hex, and grabbing the next 270 bytes, gives us the public key. We can do that in IDA, or even with a simple python script:

$ python
Python 2.7.5 (default, Aug 25 2013, 00:04:04) 
[GCC 4.2.1 Compatible Apple LLVM 5.0 (clang-500.0.68)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> f=open("AppleTV", "r")
>>> d=f.read()
>>> o=0xc98be0 - 0x1000  # must subtract a memory offset
>>> k=''
>>> for i in range(0, 270):
...   k += d[o+i]
... 
>>> import binascii
>>> binascii.b2a_hex(k)
'3082010a02820101009cd356c93bcebeb15ebecf620314fe0e15cd26a5afdcd8bf8f1790e8de426a4b1fcbe6bbc47c865f610a28437b36a78ed80844961bade02d7da942b05019f1a34c13953278e288b10b45d44fa1264db895a4589776828ba0b2499ab0bdff65e902755ae406e517ccac6b2c237a26472decf4dfa219efb026f3020df8a80ffe4f7a2de03ece182d33d11dfbdb6df7261ce5c0dd4b3f4e02f3784ba6165ecfcde44b02e7e684e3b0649e5fe13d871681501ce5d50e8841832c0f3426387b6ede7e447d3fabe808c13af4ef556bffd480cd840b54a5c81f3db29682890c727571a8394c12101ef79f4f2d2e147dbf389b7b9fcabf22144d4da21b63f269e61ecaad0203010001'
>>> 

Write that out to a file (in binary form, not hexadecimal), and use asn1parse to get the raw key specifics:

$ openssl asn1parse -in rsakey.bin -inform DER
    0:d=0  hl=4 l= 266 cons: SEQUENCE          
    4:d=1  hl=4 l= 257 prim: INTEGER           :9CD356C93BCEBEB15EBECF620314FE0E15CD26A5AFDCD8BF8F1790E8DE426A4B1FCBE6BBC47C865F610A28437B36A78ED80844961BADE02D7DA942B05019F1A34C13953278E288B10B45D44FA1264DB895A4589776828BA0B2499AB0BDFF65E902755AE406E517CCAC6B2C237A26472DECF4DFA219EFB026F3020DF8A80FFE4F7A2DE03ECE182D33D11DFBDB6DF7261CE5C0DD4B3F4E02F3784BA6165ECFCDE44B02E7E684E3B0649E5FE13D871681501CE5D50E8841832C0F3426387B6EDE7E447D3FABE808C13AF4EF556BFFD480CD840B54A5C81F3DB29682890C727571A8394C12101EF79F4F2D2E147DBF389B7B9FCABF22144D4DA21B63F269E61ECAAD
  265:d=1  hl=2 l=   3 prim: INTEGER           :010001

The long number is the modulus, and the short number is the exponent (65537).

So now we can validate the signature. For that, we could simply use some functions in the python Crypto module, but where would be the fun in that? Let’s just do it manually. In the following code, “message” is the string we want to verify, and “signature” is the signature (base-64 encoded) we pulled from StoreFront or a deviceAuthorizations setting.

from Crypto.Hash import SHA
import binascii, base64

key = '9CD356C93BCEBEB15EBECF620314FE0E15CD26A5AFDCD8BF8F1790E8DE426A4B1FCBE6BBC47C865F610A28437B36A78ED80844961BADE02D7DA942B05019F1A34C13953278E288B10B45D44FA1264DB895A4589776828BA0B2499AB0BDFF65E902755AE406E517CCAC6B2C237A26472DECF4DFA219EFB026F3020DF8A80FFE4F7A2DE03ECE182D33D11DFBDB6DF7261CE5C0DD4B3F4E02F3784BA6165ECFCDE44B02E7E684E3B0649E5FE13D871681501CE5D50E8841832C0F3426387B6EDE7E447D3FABE808C13AF4EF556BFFD480CD840B54A5C81F3DB29682890C727571A8394C12101EF79F4F2D2E147DBF389B7B9FCABF22144D4DA21B63F269E61ECAAD'

exponent = 65537

def manual_check(signature, message):
    sig = binascii.b2a_hex(base64.b64decode(signature))

    h = SHA.new(message).hexdigest()
    print "Hash: %s" % h
    m = int(key, 16)
    ct = int(sig, 16)
    pt = pow(ct, exponent, m)
    out = "%x" % pt
    print "PT: %s" % out
    check = out[-40:]
    print "Check: %s" % check
    if check == h:
        print "VERIFIED"
    else:
        print "        not verified"

signature = 'YxH4I6zha8O331odzY3Zf+APR9gYi/Atorp84x3BTqVg5N4EqAwzyh72UpiF4mgCw5CLneC/I/VlNUntZB17y6yXLstZpbRvKnr/LoQtccmLo7ELWcmFWfU3gEb7u4ne/E1N92oCHrOIxsBbnEqkOp65M47k9x6GojqDsfT4Lrr0XIJ86LH+cl2UIgVQlR77Q8fnSnvChLqGjwIdvKEi2xcfm/v40bFN0JkRV1wrEsw8Zvu3m53GKEOsLbHVCd6Waqsisopbsk3Q4j+D50EnJ699n4UlNoat0bEc4Jz8TjEHoMnB5f23NV0KlFOoC0LPVdJecbAH0bGjfD9WjgMHhA=='

message = 'iMovieNewAuthiCloudAuth'

manual_check(signature, message)

Running that code produces the following output:

Hash: 2dcd288c1ccc82c8ef7dcc17fdf3abd785c02050
PT: 1fffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffff
fffffffffffffffffffff003021300906052b0e03021a0
50004142dcd288c1ccc82c8ef7dcc17fdf3abd785c02050
Check: 2dcd288c1ccc82c8ef7dcc17fdf3abd785c02050
VERIFIED

The plaintext (“PT” above) is a DER-format signature, matching the requirements for PKCS1-v1.5. Basically, it’s:

0x00 (not seen)
0x01 (leading 0 not seen)
0xff (times "a lot") (pad the message to a pre-determined length)
0x00 (end of padding)
sig (actual signature in DER format)

The actual signature includes flags identifying it as SHA-1 based, and the actual message that was signed (the SHA1 hash). We can simply ignore everything except the 20 bytes at the end, which looks exactly like the hash we generated (2dcd288…c02050). Or if you like, we can use asn1parse again, or an online DER parser to break it all out:

SEQUENCE(2 elem)
  SEQUENCE(2 elem)
      OBJECT IDENTIFIER 1.3.14.3.2.26
      NULL
  OCTET STRING(20 byte) 2DCD288C1CCC82C8EF7DCC17FDF3ABD785C02050

(Where 1.3.14.3.2.26 corresponds to the OID for SHA-1.)

This same signature check is used for all the above-mentioned signatures:

  • javascript-url-signature
  • root-url-signature
  • icloud-auth-signature
  • addSiteDeviceAuthorizations

As I said earlier, I’m not sure the first two are being checked any longer. The third seems to be included on few of the newer applications loaded by the StoreFront call, while the last is only checked if a vendor bag is loaded by Add Site.

The signatures for the last check are stored as an array in the com.apple.frontrow “addSiteDeviceAuthorizations” setting. And, as we saw last time, the only way to add a stting to that list is with a profile signed by Apple. So the only way to make Add Site work under Apple TV 6.x (ignoring any unfortunately-still-speculative jailbreaks) is to:

  1. Retrieve the target Apple TV’s unique device identifier (udid)
  2. Using your app’s Merchant ID, create the string “<merchant><udid>”
  3. Get Apple to sign that string with the appropriate private key
  4. Include that signature in a configuration profile that enables the Add Site application
  5. Get Apple to sign the profile
  6. Install the profile on the Apple TV from step 1

Then, and only then, will you be able to load your custom application on the Apple TV.

All this leaves me with a question: “Why did Apple add all these hoops to jump through?” It’s basically a parallel to how Provisioning Profiles work for iOS developers. Was this extra level of security really necessary? As far as I know, the Add Site functionality wasn’t widely known until my talk last fall, yet these changes appeared in early iOS 7-based Apple TV betas in mid-summer 2013. Perhaps they were always on the roadmap, and Apple just couldn’t finish them in time for the previous version.

Or perhaps…is this a prelude to wider availablity of Apple TV app development? If Apple were to open up Apple TV to general development (and they’ve certainly been fielding a lot of new applications lately), then they certainly couldn’t have done that without some kind of control in place.

That’s what I’m kind of hoping: That we’ll see, in the near future, an official way for “everyday” Apple devlopers to build Apple TV apps, and to distribute them via a new “Channel Store.” Maybe this will even be unveiled with the next major Apple TV update (currently rumored for April).

I’m keeping my fingers crossed.