Posted by Amir Mazzarella on September 17, 2018
I'm interrupting yet another blog post series. I know, annoying, but I've recently become interested in FairPlay Streaming (or FPS as it will be referred to in the rest of the post) since it's the DRM used in iTunes 4K streaming. FPS is an interesting DRM because unlike most DRM implementations, the SDK is available pretty freely. For DRMs like Widevine and PlayReady, the license server SDK is only available to approved partners and to those who have signed multiple NDAs. FPS has the SDK available to download to anyone who has a paid Apple Developer account. Naturally, I acquired the latest version of the SDK (at the time the latest is v4.2.0) from https://developer.apple.com/streaming/fps and began digging. The SDK provides a reference iOS app, a reference KSM (Key Security Module: a fancy term for a license server) implementation, example license requests and licenses, a developer certificate signed by Apple, and 40 pages of documentation. All very useful. So, let's stop beating around the bush and take a look at a license request:
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
Uh oh, looks like some arbitrary base64 encoded bytes. Not entirely arbitrary though, as the document says that all license requests/responses follow C++ structs. The documentation also calls license requests "SPC messages" (SPC = Server Playback Context), so that's what I'll call them for the rest of the post. Here's the SPC struct implemented with Construct in Python:
SPC = Struct(
'version' / Int32ub,
'reserved' / Int32ub,
'iv' / Bytes(16),
'encrypted_key' / Bytes(128),
'cert_sha1_sum' / Bytes(20),
'payload_length' / Int32ub,
'payload' / Bytes(this.payload_length)
)
With names like encrypted_key
and payload
, it sure seems like the SPC is encrypted. And that it is. Before the FPS library even generates an SPC, it has to be given an "Application Certificate". The Application Certificate is a signed X.509 certificate with a 1024-bit RSA public key embedded inside that the SPC is encrypted against. Luckily, Apple provides a signed developer cert with a respective private key, as well as example SPCs that are encrypted against it. So, let's see what the parsed encrypted SPC looks like:
Container:
version = 1
reserved = 0
iv = b"]\x16D\xea\xec\x11\xf9\x83\x14uA\xe4n\xeb't"
encrypted_key = b'\x92fH\xb9\x86\x1e\xc0G\x1b\xa2\x17X\x85\x1c=\xda1\xc9;\x1d\xd6\x01\xaaN\xadD\x15\xa2\x07Y\xaa\xb9\xa6\xd8\x9fU\x13\x85\x85nsW\x17)\xdf/\x1dF\xd2\\\x13\xda*\xd7]\x00\xfd4\x13\xeb\xd9l\xa4}\x02\x95\\V\x9f\x7f\xab@\xf1\xa7\xfb#AAg\xa6S\xea\xbd\xf1\xad(=\xf5\xe0~|\xf4\xaa/\xba\xc6O\x1dF\x0f\xdf\x9a!\xee\xb2z\x7f`rxS\xa4\x14\xc1\xc4P\xc5%\xe8\xda\xb6\xa3\xf1<\xfaW\x17\x1a'
cert_sha1_sum = b'Ua\x98\n\xa5\x1f\x03\xaa\x04\xbc\xa2EV\xc8A\xa7\x176\xc7q'
payload_length = 2688
payload = b'm\xa6\xda\xe4\xee@\t\x17T{\xba\xa5\'\xb8\x82\x1b\x9c`\x81\x93+\x0f\xcf3r\x914\xf5\xf7\xa8\xf1i\x02\x1c\x04\xda\x00\xe9csZt:\x88y\xd2\xca\xb7\xbaq`\'v\xcf\x1e\xa2\xd1\xd3\xc1m\xceQ\xb2\x82D\x9c\xecs\xa0\x8e\x17"\x80\xd3\x95\x90\x04*\xfeZ\xf9UO\x97<\xeb\xf6>\x08\xe4\x0c\xd0o\xcaw\xa0\x9a\x94\xcb\xba\xdb\x97E\xe7\xf5\xa5\xf4p!<\x93\xd8[\xa5\x11\x19\xe4Q\xa1\xc95.\xd2\nL\xd942\x85\x9al\x89\x85\xbay\xa6\x00\xa9\xc6&\xfd\x8a\xad|\x0b\x1f\xaeU%\x16\x86\xf2\xcb7~h\x06\xf4c\x13\x8b\xdf\x90H\x91\xa2dE\x8e\xca\x0c\xb2\x03\xb7`\xd8\xc5\xad!#\xd1\xbfi\xba\\\x9c{To\xff\xd3*\x9b\xc7\x02\xdda\xaao\x8f\xc1>y\x8c\x11D\xa2\x1e\x82\xc0t\xd9p-pBD\xd0\xec2\xe1\x926\xc4A\xc0\xb8\xd6\xa8\xa2\xf8\x05U\x82e\x9e\xb6\x16\x97t\x93x\xa9%mO\xfe\xb0\x90\xae*\xa1bNYZ\xb26tj\xdc\xa5w\xa6[`a_\x17\xee\x9cV(\x98\x8c\xea\xaco\x03\n4\xbc}\x0fY\xb2y\xa9\xa3\xede\x90\xd8\xdc\xa1\x18U\xb6[\xd9\xcb\xdc\xb1\x05\xa6\xbd\xd2\xcfSBh$\xab\xdb\'\x97I\xa1\xc4\x15L\xa5\xb2\x03\xf3b8\x1f\x98rZ\x87\xcd]:\xe2Fo\x93%\x9f\xb8"\x87\x14\x0b\xa0^\xe9\xe3\xeeA\xda\xb5\'\xcc\xee\x02\xcfe\x9e\xcb*\xbd\x98(%u\x90`\x03\x89\xdfp\xf6\x115f\xfac\xd6R\xd6n\xaf\xc2\xec9\x14\x10-\xa4\'\xb3\xdb\xc2[\x16zE\x19\x03J4nh\x9e\xa8\x0e\xbc\xa5q2,T\xbe\xe3D\x84zC\xf0\xdf\x12\x7f\x9e0h\xfc\xf0T\x1fN\x8e\xb3\x11\xfb=S\xdd!\xce\xfb\x19"\xfe,\xc71b\x9e\xcdMp\x91\xb9\xe6\x99\xa7~\xee\x1a\xc9\x00\xb9\xc9\xde\xf5\xbb\'\x19\x1fm5k{\x9c M\x97\x8c\xda\x9cO\x8e\xac\xe2G\n\x07\xcb\xd3\x06\xcc @\rr]<\x83Kds\xbe\x96X\x03TC\xf3\xc6n1/A+\x1e\xe7-q\x1cm\x1a\x13#\x0cY\r\xaca\xbd\xc4\x12*1\x83\xedO\xc89%=\xdf\x92\x87\xd0\r\x89\x96E\xb7\x07\x1b\xedE\xb0>\x12&\xd0\xc0q\x96\xb1Xu\xa7P\xb4!\xb5\xa1\xf2\xbe\xb2\xd2BX\x11\xbc\x0e)\xd0\xb2\xb3-\xab3\xf2\xc0\xeb\n6]\x10\x10O:,B\xdbq\xa9\xdb\xc4\x9b}\xd1\xa4]0\x80\xc5\xa3w\x19F\xe4\x98C\x0ev<b\'OX\x10\xf8Z\x81O\xe8\xa5UFER$\r\xd3\xea\xcd\xf4Ry\xde\xe18\xc9\x87\x98\xf0\xad0E\x97\xc9|\xb34\x8f\xf1\xa7\x97_\\\xc2\x83<\x87\xbb\x1e<\xa6\x00\x88\xafF&\xddy\xba\x08TJk<\xca]\xa5\xc39]\xbe\xc7\x1dh\xcf\x89\xcce\x98\xd2\xad\x11?\x18\xab\xf4\x17Y\'g\\\xb2\xb2\x82\xb1E\xb5\x18*\x02>\x08\xe40\x18\x83\xdee\xa1\xba\x92A9pffk\xda7\x1f{\\\x94\xde\xc9v\x92X}@a\x9ar\xaa\xfb&\x9bmh\xd3G!%%\xdf\xf4\x05\x88z\xe4C\x1bd6\xd9\x1f#\x00\xe6\xdf\xcc\x93%\x83\xc5\xda-\xe6\xd8V\xc9\xd4mh\xebD\xcf\xd22\xd1\x1d\x1e\x0f\xbb`]6~H\x03\xa9\xe7\x889\x99\xbd\\\xcb\x15\xf1\xd6@\xae\x08\t\xf2\xa0\x07\xf9\r\xdf\xbbL\x7f\xe5\xbd\xc4\xeb\x90\x1b\xddQ\xb9!B\xb7-\x12W\x94\x97\xa5M\xe4\xe9\xe1y\xcc\x89\xfb\xf9\x04!A\x07\x89O\xa1\xae\xc5\x94\xeb\xf1Af\xc4\x10^:w\x14)\xa0\xb4\x19\x19X\xea\x1ddS,[\x96\xab\x80\x9a\xf1\xcf2\x1f\xbe_\xea\xb8d\xbev\x11dl\xe1|\x96\x01\xa9\xb1\xf6M!,\xfa\xbd\xc4\x9d\x88\xe0T\xd0\xe6\x82l\rt0\'(\xa8k\xd1-N\xd9]\x9el\xa8\xa9W\xd4\x15\xf9\x87j|\x7f`+i\xc2\xe7\x8b\x0c\x89\xe9\x0f\xb8\r\x1e\xceI\x03\xcb\xf2\xa4\x97P\xf8u\xc4O\xee\x8f\xa5\xda\x94\xdf\xb3\xc1\x1fIl\xc8\rx,\x8c\x14D}\x17\x17\x7f`+\xb2\x02\x14S:\x89-W\x17\xc01\xea]k\xaa+\xbd\x80\xbf1}%\x0e\xb6\x00\xaaA\x83\xad\xba"\xd0+}\x89\xb7\xc1P^\xea\x0f\xa6\x13e\xe7\xd2w\x9du\xec9\xd2\xfc\x10#2Y9,\xc2\x10\x04\xdea5\xe7jh\x10\x02 \xff\xc2\xed\x85\xc4\x8cU\xca\xda\xa5\x8d@9E:Pl?}Y\xc8:\x82\x9b\xae\xbf\x85\xbd \x1a\x9e\x05\x8ab\x15pMF\xd4e\x92J\xfaY\xa5+}\x1b\x9f\x9bS\xfb6\xc8_\xe5=\xd5\x833\xb0\xfbs\x84\xaeb\x1d\x9bM7\xc1\n\x02T\x02\x8f\xef\xaa\x80G\x95\xc3\xd0C\xe7\xff\xfd\xda\x1b4W\x81k)\x9b\xf8\xd2\xa1\xea\xc2T\xff\xc3\xa4{\xbf\xd0\xb3\xba7\xf6M\xf1\xc2\xff{\xee\xb6\x15\x9a\xc8\x19/\xa1\xd2o\xda\xb2X\xe7k&DT\xac\x852F\xd2\xfe2~-\xe1K\x8aA\xaa\x96\xfb\xc6\x08\x1b\xec\xe8\x9b+Y@\xbf\x9eV\xd0\x8c(w\xf6Y%QW\x11\x80PSl\xec<h]e\xdc\xe5?\xe3\xdb-y\x8eOjgc\xa1|\xe8\xcf"\\\xb7bE` S\xa2\xe1+\xab+2\xe0\xb3/\xfe6\xfa\x82d\xb7\x80\xd7\x9dUy\xc5?5\xf46\xd3\x11V\xd5\xd7\xae\xf3S\x07\xa7{\xd8\xf2\xb6\xbb\x9a\x87\x11m\x8b\x83\x05g\xaa\x82\xd7c\x9d\xd3\x8b|\x01\xcbmv3\xd7\xc7\xac\x9esX\xcdj\xb6uh\xac\xa1\xcajP\x9e\xbc\xdd\x80\xfe_\x993\xd5\xe3\x8a*A\x96\x8f\x1e\x95\xaf\xd5IS\xef\xe4R\x10\x11\xb0\x10\xd5"\xa0D\xf0\xe5\xf28\x8d\xfe>\xa2e7\xb8\xbb\xb9T\xd3\xdb\xc7\x12\xc0YY7\n\xc1\x14\x97\xd3\xe5\xf1l\x96o&\xaa\xca\xbb\xab\x05\x14(\xcd`\x8b\xef\xaa@\xdd\xf9Y\xce\xa1\xc9\x95\xbc\x88\xc6\x8bgL7w$\xde\'\xee1)\xa1`\xa5\t\x9a`\xadg\xc3\x93>\xd9tz\xad\x81\xdf\x13Z\xedC\x98\x98\x0c\xd5\xadv&\xaa\x81)\nQJ\xb6\x05\x1b\x99V\xd0\xccB\'\xf0Ho\x8f\xd3\xe8I=\xfex\x93\x8e\x9b`\xf5\xfa\xdaw\xb7\xa4\xd6\xe0\xab\xe6,\xd2_\x10\xad3\xa3\xcd=y\xe1\xe5\r\xcd\xf8\x7f\xfe\x01\xf7\xe5=\x003_8PC>5!\x18\x01)/\x9bT\xef\x08\tG2\xbc\x1bi}\xab\x13dl\xf1(\xd8\xb6\x15EaY\xaa}\xca\t\xf7\x82 \x1d\xf1\x12\xf0s\xccQ\xe0\x9e\xa8\xc8\xe6\xff\xed\x1aJ\x06\xf8F\xc0YX\x10\xa7}\xf9$\x0c\xeb)}\x12\x15\xa2\x88P\xf5\xd1\xc4\x8f\x12\xb7\xcb\x879lZN\xa6\x9e\xee\xb3\xdb\x97fR4\xa5Q\xb4n\xfaZUE\x13\xb1\xd3\xda\x99\xc5\xcbXK%\x04\xef\xd8\x9f\xb4)\x90\x81\xee\xf8k\x0bf\xdf\xd1i\xde\xdd\x17\xc8\x19\xa0|X;u\xe4\x0c}?\xe5\xa7\xf0`^\xd0\x1b\x11&k\x84BL\xd4=n\xd6\xaej\x83M\xc8%\x7f\xa6\x15J\x9b`p\xb8F6w\xae\xaf%\xd5#\x84#-\xc5\xbc\xebQ\x1c\xd5\t\x84k\xa0\xa3\xb9\x9cA\x8e\'j\xfc\x96\xf6\xca\x7f\xb2B\xa1\xd2\xf2\x9d\x98\x89/x\x83\xe2@\x9d\x1a\xf7\x8e\x80\t\r\x06\x9b\xcd6\xd9\xf8\xc45]\xc94S\xba\xacr\xf2\xb9K\xf5\xfc\x08\x152\xc9\x19\xde\xf5\x1f\xed\x8c\xccjv\xa9 D\xf9m\xbd\x02m\x9d\n\r\xf5\x08\xab\xc8f\x99\'\xc4\x1b\\/\xd3\x9f\x8c\xbdSW\xb4\xe4l\x9c^\x1e|\xf5Al\xfb\x10\xc8MG\xb2\xa8\x844\xb2e\xeb\xf0\xb7\x9c7O^m\x97=\xed\x88\x87d\xf2\xfe\xc4\x19\x19\xdd\xf0~\x99\x10\xa7\x7f`\xe3OZgS\xc6D\xab\xed\xb9\xdcZ#\xcc\xb69\x82\xf5TW\xe1\xafa\x11\xf9\x8bP?Gm\x00\x8a0:\x08\xf9-tQ+\xd4\xdd\x91<\xe6\x7fk\xa4\xdc~o\x00^~&0\x16\xfa\xe1\x94\x07\xf2K\x7f{V\x13q\xf3\xfaWL\xbb\xb7\x8bi\xa9$h\x8b\xf5\'\xc2C:R\x84s\x03\',\'\xd3\x8b\xf3\r\xc8\xc9s\xf6\x90em\x9f\x8b\x98\x1e\xcf\x94\xc1\xfd4i\xa4\x1d\xb6\x7f\x02\xc2\xaa\x0c\xf6\xc3\x97\xa8\x8a\x04\xe7\x86\x1c\xea`\xd0u\x19\xa9\xd77Q\xe3t;<\xd6\xd5V\xb2`1|53\xea1+N\x0e\xdd\xa4\xb9\xd5!\x95\xff\x0c\xd2bt<1\xa6\xc2X#~\x86\xba\x03\xbc\x14b(]\xf7\xbc$\x01C\x8d\x8d\x91"\x8d\xc4\x9f\x06\n\xac\x8a\xf4v\x89\xe2\x0f@B\xc7jF\xc0\x18\x1bo\xf6\x99\x01\x11\xed\xa7Y\xc8\xa3\xc2v\xd0\x00\x02e>\xbcH?\xf1<\xe4qt\xd9G\xba\xed\xf5\x93\x99^\xc9\x1bo\x85}\xf9\xd37m\xf9\xcb\xb0v,\xb8\xc7\xc7\x00\xea4\x92\x92\xf3F\x94-\xea\xe7|u\xb0\x9bs\xc8\x8b\xcc\xb8\x17\x1e\xe3\x8f%\xe4\xd8Xp\xec\x87\xdf\xba\xee[\x01\x9a\xae\x08\x8ctL\x07\x81\xbb\xc8\xa3\xa7\xab\x1a\xf4U\xd6\x85\x05jCY\tw\xff\xc9\x80<\xa3\x8byAP\x90\xa1\x11\x986\xe46\x05\x8f\xe5\xfa\xe9\xb2*\x94N\xa6\xba\xb6=\x80\xf2\xdc\xc4\xad\x9f\x02\xc0B\r_\xc5\xf7\x9c*R\x84J^\xc9 h\x8a\x8e\xb2\x8a\xe1\xb4\x99\xdc\x90\x03\xf8\xbd\xb7\xa8Z\x189\x8e%\xe4Xi&\xcf\x94\x0f\xb6\xd6\xf2:\n\x9d&\x0f\xd4\xa5\xbf\x10~\x1f\x05\x04\x1c\n\xbd>\x19\xdf\xf7\x8d1&\xf3\x07\xc0\x00\xfa\xb5s\xebJ\x13\xa2v\xb8\x06\xbb\x08V\x9c\xae\xda~\xfa\xb7\xe3{\x9d\'Q\x12\xb3\xc649\x026\xc7\xe0y\xcdm"f\x11\xb5\xd9#\xc5f\x89L\x90\xa7\x9e\x98\xc8e\xd8\x06HH\xcb\xe7\xb7]\x95Qz\x894\x82P\xf3\tI\xdc4=\x1c\x12$\xc3\xc36\x12P\xc0If9;rZ\xa1D\xf3\xf9\x90\x91\xdb\xe7\xddg\xf6\x0f}4G\xb5\xdf\xb3\xc93#5\x00\xb3\x1a\xec#P:?U.-Y\x97\x11L\xbb,\xffBE\xad\x1f<6\xef\x91\xe8Y\x9dC8i?\n\xba\x8cS\x0f\xb0\xefD\x12\r\xab\xcb+\xa0`c^@4\xdecs\xd6\x97c\xbf\r\x80\xcf\xca\xda\x7f\xf2~\xf3\x12\xd4\x96x\xcc\x8a\xae\xb1\xa6\x0b\x97\xda\xbb\xeb\x9e\xc8\xaa\xe3\xf5\x1f\xdd\xb0-\xf1X\xd0\x9b\xae\x07s\x9f\xde\x9a\x99\xa5Y@\xaf-G[\xceO\xd7\xd6\x15{\x92`(\\M\x89.\xdd\r\x1b\x04\x86zI\xe5_\xa4?\xb6\xe8\x1b\xed5\x8e$\xd2\x019;\x14\xae\x95\xeb\xfb\xc5\xac\x10/\xa2\x91_K\x99\x9fd)\x14\xa1$\xb9\x1d\xd3\xbb\x86C5\xb6C\x16\xf7\xa9\x1e\xf5\x15Xp\x13\xeaM\xc7\xbd\xd0\xdb\xee\x80\xef\xd6\xa8\x01\xfaC\xecM\x07\x99?"\x83\xb0\x9a\xbbl\x16\xf1\xb3R:\';|\x83\xdc\xb7\xeb\xdb\x1b\x93\x81\xab\xf0\x92\x12j\xa1\x92\xea\xf7\x98H\x97\xc9\x1e\xa6t+\x8bF\xd5\xbe\x16\xcc\xe4\x80A\x0e\xec\x02X\xdb\x9d\xf4\x9a\x96'
The SPC looks pretty standard. The payload seems to be AES encrypted by a key that is in the SPC under encrypted_key
. The documentation confirms that key is then encrypted RSA-OAEP against the application cert. Since the SPC is encrypted against a cert we have the private key to, let's decrypt it. But before we do, let's define the struct for the decrypted payload so we can jump straight to parsing it. SPC and CKC (Content Key Context: fancy term for license) payloads are composed of what's called TLLV (Tag Length Length Value: similar to ASN.1 (tag length value) blocks) blocks. Those blocks each contain an 8-byte tag used for identifying what kind of block it is, a 4-byte integer that tells how long the entire block is, a 4-byte integer that tells how long the value of the block is, and padding to make the block length a multiple of 16. The padding isn't standard; it's just random bytes. Padding is calculated by subtracting the block length by the value length. Here's the struct for a TLLV block:
TLLV = GreedyRange(Struct(
'tag' / Bytes(8),
Embedded(Switch(this.tag, {
b'\x3D\x1A\x10\xB8\xBF\xFA\xC2\xEC': SK_R1,
b'\x58\xB3\x81\x65\xAF\x0E\x3D\x5A': ENCRYPTED_CK,
}, default=RAW_TLLV_BLOCK))
))
And a RAW_TLLV_BLOCK
:
RAW_TLLV_BLOCK = Struct(
'tag_name' / Switch(this._.tag, {
# SPC Tags
b'\xB3\x49\xD4\x80\x9E\x91\x06\x87': Computed('SK_R1_INTEGRITY'),
b'\x89\xC9\x0F\x12\x20\x41\x06\xB2': Computed('AR_SEED'),
b'\x71\xB5\x59\x5A\xC1\x52\x11\x33': Computed('R2'),
b'\x19\xF9\xD4\xE5\xAB\x76\x09\xCB': Computed('RETURN_REQUEST'),
b'\x1B\xF7\xF5\x3F\x5D\x5D\x5A\x1F': Computed('ASSET_ID'),
b'\x47\xAA\x7A\xD3\x44\x05\x77\xDE': Computed('TRANSACTION_ID'),
b'\x67\xB8\xFB\x79\xEC\xCE\x1A\x13': Computed('PROTOCOL_VERSIONS_SUPPORTED'),
b'\x5D\x81\xBC\xBC\xC7\xF6\x17\x03': Computed('PROTOCOL_VERSION_USED'),
b'\xAB\xB0\x25\x6A\x31\x84\x39\x74': Computed('STREAMING_INDICATOR'),
b'\xEB\x8E\xFD\xF2\xB2\x5A\xB3\xA0': Computed('MEDIA_PLAYBACK_STATE'),
# CKC Tags
b'\x47\xAC\xF6\xA4\x18\xCD\x09\x1A': Computed('CONTENT_KEY_DURATION'),
b'\x2E\x52\xF1\x53\x0D\x8D\xDB\x4A': Computed('HDCP_ENFORCEMENT')
}, default=Computed('unknown')),
'block_length' / Int32ub,
'value_length' / Int32ub,
'value' / Bytes(this.value_length),
'padding' / Bytes(this.block_length - this.value_length)
)
In Construct, Computed
refers to something that isn't in the stream, it's just for the parser. i.e., I'm using Computed
to determine what kind of tag it is by a string instead of checking the arbitrary 8 bytes.
Those tag names look weird, I know, but the documentation has a table with all the tag names and their byte values, so I just ported that over:
Before we define the functions of each tag, let's now take a look at the parsed decrypted payload:
ListContainer:
Container:
tag = b'\xb3I\xd4\x80\x9e\x91\x06\x87'
tag_name = SK_R1_INTEGRITY
block_length = 64
value_length = 16
value = b'T\xa1k\xe0\x13~\xf2Y\xab>O\xc7\x96\x90\x82_'
padding = b'X\xaa6J\x88[\xbb\x15=8\xeb\xddF\xe4oJ\x97\xe92z\xf9\x9b\xe2\xa6\x83\xde*\x19\xba\xebT%\x1a\xf4\x8b\x8d\x95;d\xbf[\xf8}\x01A%a\xdd'
Container:
tag = b'\xf9\x11\xf0M\xa5K\xf5\x99'
tag_name = unknown
block_length = 128
value_length = 121
value = b'\xd9\xa2\xd8N\xbf\x8e\x07\xfaO\x890&\x1f\xf9dcCvk|\xe0\\8\x88\x89\xa8\xed\xd8\rKCRj#\x93)\x1ec\x03\xef\x14\x8a\x13/-\xdex\xcdt\x073\xb7\xb0\r\\wPY\xd6\x8b51\xbeU@0\xbe\x013\x84\xff\x122\xc84\xb1\xa3\xe6\x17\x9bW\x0bn\x9e\x1d\x86\x9fu\xe3\xd1\xe6[\x10E\x1ae`\xea\xd4\xf7\xa2v\xee\xe4\x9e\xf2\xff\x0e\xefMa\x1e+\x8d\x9dnpf\xf1Ms'
padding = b'r\x0fr\\\x1a\x08G'
Container:
tag = b'\xba\x08\xcct\xda\xc9\x17m'
tag_name = unknown
block_length = 32
value_length = 25
value = b'\xfbJv\x13\xe3\xf8H;\xd0\x1e\x1a\xae\x7f\\\xe0\x90,\x00\xc8\xcbg[b$\x85'
padding = b'I\xd4\xe2\x12gh\xea'
Container:
tag = b'=\x1a\x10\xb8\xbf\xfa\xc2\xec'
tag_name = SK_R1
block_length = 256
value_length = 112
iv = b'OE\xd8\\\xe2bs\x10\x1a\x97\xf30\x81\xc1\xd0J'
payload = b'\x93\xb2\xdd\x03U\xe3cr\x9d\x92\xa4ZE\xce\x8d%\x8b\x0c\x08\xaae\x1c\td\x97k\xf0\x94M(%\xf3\xac\x8d\xde~\xd21O\xa0\xef?\xb4[\x97\xa2&\xe8\xc56m\xef\xe5\xf1\xe1+\xd7\xb7!\x98\xa4\xa8\xf2e:\x0e\xf0\xde\x8c7\xa4|<@\xf0\x12\xe1\\\x8bY=\xf1-K\x01`:\x975~j\xe0\xa1\x1c\xa3\xe3'
padding = b'\xbax\xb2\xde"\xa2\x9d\xbc\xffz\xf3Ni\xbeE\xd7\xae<\xfa\x15V\x80h\x06\xc0\x88\x94g\xc3\x066)f\xe6\xcc\n\xf3k\x91,\x85"\x0cy\x13\xb6\x97\x87\xda\x98\xca\xa6Z\xddA#\xd1\x81iMK\x9e\xea\x04A\xfabQr \xf6\x1e%\xe1KZ\x10\xc3\x9eV\x85\x8a\xe86\xdc.\xb1o\xd5\x90\x12m\xe8\xdf\xc3\xaay\x954wa\r\xde\x90\xc5\x066&R\xe4\xb0G\xc3b?\x95\x1b\xc5\xccLj\xf9\x16\xb7\x84\xca\xb5\xed\xd2\xe3\xb1\xdb\xbbg(\xdc\xef6@\xedh}\xbf\xd4'
Container:
tag = b'\xc1\xcd\xe5\x9b\x14b\x9a*'
tag_name = unknown
block_length = 32
value_length = 1
value = b'B'
padding = b"'\xcaA}\xbdV\xe6\x1d\xdf\xb8B\x92\xb0\xf5H\xd2\xa8\x19\xf2\xa9|\xf4T\xfa\xa2\x14\xd1$\x87\xb9\xf4"
Container:
tag = b'\x85$(\x9du\xb9\xe9\x0e'
tag_name = unknown
block_length = 48
value_length = 1
value = b'w'
padding = b'\n`3`\x83q\xe78\x07\xa0J\xa1"\xd1Sbo\xd1y\xc2\x90X\xa3\xfd\xc7=\tNG_\x96K\xa7j\x0f\xdb\x1e\xed\xdf\n\xfaL\xdf\xa8U\xafX'
Container:
tag = b'\x89\xc9\x0f\x12 A\x06\xb2'
tag_name = AR_SEED
block_length = 208
value_length = 16
value = b"\xf3\xc6\x9d\x1e\x8c\xc4'Zm2\x86\xd32a>\x13"
padding = b'C\xee=\xe0\xd4\x04\xab\xf46\xea\xaf-\xe2\xa3\xfc\x07\x98\x94Xai\x96$\'W\xea\x9dO\xdd\xa23\xafll\x81Q+"\x15w\xa3\xc5Q\x97aT\xf3v\xa6|\x902M*\xe7\xa1\x055\x81b\xc9\xe0\xe8\xe5\xf8\xa2\x08A\x7f\xe94\'>\x9d\x9foU\xe9\xf0$E\xbc\xfe(\x7fNm\x81\xd9\xcfY\x0b\x86(\xbd\x0bw\xc1}wR\x8dV@f\xe8\t\xe9\x13\x84\xa1\xa7to\x1dT\xd9\xb7\xbb\xd1\x17\xaf\x9bt\xed\xd7,n\xf5}\x18 \xd0W\xa1\xb2\xfdz\xd8\x9eEUD\xc10\x03\x87T\xe3\xdcWN\x03a\x91\xbf#\x80\xfda~\xfcx\xb4t\xcf\xa2\xed=\x07\xc9l\xa7?2\xfd\x90I\xa7\x9e?P\xcd\x8a\xd4{\x83l%\x9b\xb5F'
Container:
tag = b'\x13\r\x99L\xb8\x94\xb9\xe3'
tag_name = unknown
block_length = 32
value_length = 19
value = b'\xa5\x13T\xe5\x1a\xa0[R\xdf\x0ck\xa5\x0f\x94\x8d\\e\xf56'
padding = b'\xe2\xddh\xd085\xcd/\xabj\xbdU\xfe'
Container:
tag = b'\x9b}>\xc9L\xd3\xce\x82'
tag_name = unknown
block_length = 32
value_length = 19
value = b'\xc0\x9d\x91 \xe5\x11\x11\xbd\xa2\xde\xdd0\xcf\t\xce\x178;\xf8'
padding = b'$\xc5\x9e\x94\xf6\xf24\xea\xf7\x80\xb6\x91\x01'
Container:
tag = b'\xb7\xe3\x9a\x12\xd2\x1b-\xb4'
tag_name = unknown
block_length = 48
value_length = 19
value = b'o\xc3)\x9ae\xcc\xe8F\x8c\xac\xd1\xff1&\x0caOYP'
padding = b'^Ip:E\xab&\xf06\x03\xdd\xffF(p\xf3\xa6:\xe3\x05\xb7\xb5\xbb\xd2\x06V?j}'
Container:
tag = b'q\xb5YZ\xc1R\x113'
tag_name = R2
block_length = 176
value_length = 21
value = b'\x11\xf7\xbea,\xa9^\xf5\xe0\x07\xceQ\x89j\xe4P,\xa3\xd8\x80\x1b'
padding = b"_\x99\x19j\xcep\xda1S<g\x88\xa1\x9a\x1e\xa1\xa1\x90\x0e\x9dR\xd0\x92\x8a\xcf\x10 7\xe8Y\x18\xab\x96\x84\xebq\x1ap\x06\x80\xca\x83\x9c\x99\xb5C\x08\x12\xac\xab\x05\xc2\xe9YR4\x1f\xe7:\x0f\xeet\x0bW\xb6\x136Nq\xbfX\x1fN(\x9d?\xa9\x98\x92\x9c'\xde\xc3\xe4\xdeX\xbb\xccG-\xb9\x11;\xfe\xbb\x1a9\xfa\x1f\x1c\x99<\xb0\xd0\xb4*\x978]1\xb0\xc6\xe5\xf9|\x18\x02\\\xbbmjG\xf3\xb8J\x0e\xd1\xdd\xa5*O\x80\xbbt%\x0f\xb8\x7f\\+\xfc)/\x94!\xbf\x0eC6\xae\x07\x07\xe5\x17\xbc"
Container:
tag = b'v\xc1\x9e\xc7\x17%\xc7\x14'
tag_name = unknown
block_length = 80
value_length = 38
value = b'\x07\x17\x05\xd8\xcb\xeb0.\xc2\x01\x16\xeanQ\xc7\xf9"\xef%x\x08O\xb1\xbaGC8e\xb0\xfa\x98\xee?\xb3\x84\xb3,!'
padding = b'Q\x1f\x90\t\xca\xbd\xeeY\xb2YT\xdd\xc3\xeaN\xa2\x06\x9c,\xaf\xc9\xce\xfcCsu\xee\xc9\xf4\xa2\xfc2$O\xbf\x90;\x0c7\xa4\xcb\x06'
Container:
tag = b'\x1b\xf7\xf5?]]Z\x1f'
tag_name = ASSET_ID
block_length = 128
value_length = 18
value = b'\xaa\xbb\xcc\xdd\xee\xff\xaa\xbb\xcc\xdd\xee\xff\xaa\xbb\xcc\xdd\xee\xff'
padding = b"rU6>)\xd1RSE\xab\xf4\xc5\x88\xa1\xf7\xa4\x02\xa5w\x17\xe1\x96\xae\xc84\xf2\x93=\xb5\xfc$X\xc7\x94\x1b\xbeso\xa9\xda\xa27\xd21#\x8f\xf8\xe2\x13\x9f\xbcy\xbb\xd3\xf5u]i0\xa6\r\x8ahmk\x11\xef\xacg\x0f\xd0\xf0\x12\xa05\xbd\xba\x08\xb1\x86\x8b(\xc6'\xa8\x83T\xb4Z\xbfAxO\xb22\x1ea\x85\xe6\xfaV\x8cGn\x90\xaf\xb9\x15\xb6\xff"
Container:
tag = b'f\xc8#\xf3y\xb8{\xb5'
tag_name = unknown
block_length = 48
value_length = 6
value = b'\xc9\x9f\xb9\x1d%}'
padding = b'\xdf\xb6\x80\x98\xd5z\xe2\xe0\xc9\xa5 m\x1bU\xb4\x9f\xe5\xd8D\x97_\x1dL\xee\x96\xd4dc\xe4\xe4\xadM\x0f\x9byR\x83\xa4\xf1w\xf3+'
Container:
tag = b'\x18\xd4,_\x8eTZK'
tag_name = unknown
block_length = 16
value_length = 6
value = b'\x96\xf8\xb2\xc4\xf7\xa4'
padding = b'&\n\x13$6\xf0\x93\xa7\x054'
Container:
tag = b'G\xaaz\xd3D\x05w\xde'
tag_name = TRANSACTION_ID
block_length = 112
value_length = 8
value = b'\x14s\xe5\xccS\xe1\xe5\xd6'
padding = b'\x01x\xe7\xe8\x9b\xc6[y\xe7\xf9-&\x11\xbdKW\x91Z4[O\x07\xd3\xcb\xc60\xf6\xd5o7l\xe0\xfb\xc6\x05\xc9\x93\x1a\x0c\x91\xe4\x00\x01\xf7\xde\x0cr-R\xe8M\xc6\x86\x82\x94x;I\xa9\xfa\x98G\x94\x04!\x8d1\xd0\n\xeb\xbd2\x10\xab/\x0cO\xb9\x1cf\x08Ev\xa6\x0fe\x08s\xbf\xd1\x17\xa51\xe2M\xe8\xa1\xd7\x8fE\x1b\x85\xb6\x1f'
Container:
tag = b'\x80\xfc\xc5(^\xd8\xcb\xae'
tag_name = unknown
block_length = 80
value_length = 48
value = b'a\xf31V\x1c\xbe\x7f\xccL(5\xe6O\x8a\xf8\xe0I\xf02\xbf\xee\xdd_\x7f\xc7\xe7\x94?\xe9.\xa5\xc9\xfaKG\r\xd1\xaf\xc5\xab\x1a\x19\x9b\xbc\xf2\xe3c\xe9'
padding = b'\x02\x85g\x98\xea\xc8\xec\xfe\xe4\xee9\xb4P|\xfeE\xa2\t\x7f\xac\xb5P\xf2\xda!J:Q3\xe4\xb5\x19'
Container:
tag = b']\x81\xbc\xbc\xc7\xf6\x17\x03'
tag_name = PROTOCOL_VERSION_USED
block_length = 192
value_length = 4
value = b'\x00\x00\x00\x01'
padding = b'R\x90,\xf9\xeeq\xe0i\xc5*<\x9a8\x87\x9bV#\x01\x1b\x18\xf7$0\x15\xbe\xe6A\xb9q\x14{\x01<\xa8P\x7f\x07Xq\x9b\r\x8f\xac\xc0 \xec\xdd\xbb\xda\x88\xc1\xfa\x11[\x9b\xac\t\xc0\x86\x9f\x9acX\xf2\x85\xde\x80\xb8\x12\x84\t$?XdU$\xbb5A\xaf\xc1W\xf2\xdc\xc8\x03\x86\xb7"\xa7\x02\xff\xc8\xbf8\x87\x04\xeb\x93V\xef\xb4\xb6^C\\|\r\n\x96\x9b\xfawxEm\xf1y\x94\xfc\x16\xbe\xcd\xe1\xa6o+f\xf3m.&\x17PK+\x135\x8bc\x19\xbb\x1bN\xc9\xe3z\xd8\x9f2`\xa6\xa8+\\\x1e#\xdc\xe8\x9fTu\xe7\x83\xce\xbc\x89\x99j\xa8"\x01zO\t\xa2%\x89|{\xb5\xd5\x8d\xb2\xe5Wi'
Container:
tag = b'\xfeq\xba\xef\x123ss'
tag_name = unknown
block_length = 48
value_length = 14
value = b'\x01/+\x02\xb76/\xd2\x81\xca\xf3\x9b\xd12'
padding = b"8\xd2N=q\x81\x89\xb4Ak\xf3P'(<m\xea\xed\x87\xe2\x15\xb0Dw\x8a\rq\x85#\xf1\x14z\x12\xf3"
Container:
tag = b'g\xb8\xfby\xec\xce\x1a\x13'
tag_name = PROTOCOL_VERSIONS_SUPPORTED
block_length = 160
value_length = 4
value = b'\x00\x00\x00\x01'
padding = b'\xb0r\x9c_\xf6\xd1-\xa7\x8e\x962$I{\xcb\x84+\xd2>\xbb\xcf\xe0r\xf1\xabz\xd7\x84\x90\x15+!\xdbY\xf9\xf4KCX\xee\xb6\xbfvO\\\x96|\xa2\xf10&8dYp+l\\1\xfb\xee\x06^q]\xe4\xe7\x9a\xaf\xbe\rX\xdfH\x06\xb7\xb4l\xc2@g8"\x87\xfcs\x91\xdcM\xc1\xfch]\xc5\xec\x08E-\x18\x1fC|\xb9\xe6B\xe1\xa3n\x19\xb6\x89tH\x8f\x0b6\x86ah{T\x0eMQe\x9fA3\xbe\x96\x85\x83H\x99U\n"\xe8=\xa1:6\xd3\xca\xd3\xb0e%\xcd\x95\xc1\xde\xc1\x9f\xf8?'
Container:
tag = b'\xe0?v+\xa8L\xc8\xe0'
tag_name = unknown
block_length = 16
value_length = 0
value = b''
padding = b'\xb3\x8b\xaa\xda\x9dt\xfai\xd5\x93\xad\x86\xef\xd3n\x1e'
Container:
tag = b'\x90\xac\x15=\xac\xff\x04\x16'
tag_name = unknown
block_length = 80
value_length = 16
value = b'\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\n'
padding = b'\xe4\x8c\x01\x8bw\x0c\xe5@R\x07\x86\xc6\xef\x82\xd1\x0f7I\xa5\x88\x98\xca}\x04z\xfa1\xfb\n\x1eK2\xa6\xab\x9f\xe6Oc\xb3f\xd4\xf4v\x8a\x0f\xec\xe9s\xbf\xd1u\xa5\xecnM\xb7i\xd7\x19u\xed\xe2\xb6\x18'
Container:
tag = b'\xe1\xfcM(\xc0\xd7\xcb\xe5'
tag_name = unknown
block_length = 32
value_length = 1
value = b'a'
padding = b'^1i\x97\xbe\x92\x08LV\x18\xe6M\x8au3IH2\x83-\xdd\xdd"\xc7\x91W?z.p\xc3'
Container:
tag = b'\x82\xc2\xa5\x9e\x98\x9c\xd8\x17'
tag_name = unknown
block_length = 64
value_length = 16
value = b'1\x9f\xceg\x17\x8d\x11?\xdf\xb9\x8f\xcc\xe6c\xbf\xfc'
padding = b'Y\xc7\xeb\xf1\x8a\xf5\x0f\xd7Y\xa1\xd3\xc42\x7f\x80t\xe8q\xf5<)W;5L\x9bK-\x10O\x15V\x82\xb1\x13\x15\x99\x90\xf3\xbe\xee\x88bi\xcf*6['
Container:
tag = b'\x19\xf9\xd4\xe5\xabv\t\xcb'
tag_name = RETURN_REQUEST
block_length = 96
value_length = 56
value = b'\x1b\xf7\xf5?]]Z\x1fG\xaaz\xd3D\x05w\xde\xf9\x11\xf0M\xa5K\xf5\x99\xba\x08\xcct\xda\xc9\x17m\x13\r\x99L\xb8\x94\xb9\xe3f\xc8#\xf3y\xb8{\xb5\x18\xd4,_\x8eTZK'
padding = b'q\xc8O\x97\xc0q\xa7C\xff\x9b\xbf\x95n\x04\xf4TV\xf9\x89\xb6sC\xb5\xf1\r\xfc\x96\xc0\xd6w\nkmf\x14\x94\x8b\x16\x95d'
Container:
tag = b'\xda\xaa\x02\xb9B\x14Y\x01'
tag_name = unknown
block_length = 64
value_length = 26
value = b'y\xcf\xd6\x8eji\xdaU\xfd\xdb\xc5\t\xf4G.W\x19\xed\xc6\xb57\x1d\x88\xa1\xf0%'
padding = b'\x14\x10$\xd8\x9d\xf4h\xb4\xf1T+@tQ\x8b^i3\x9e\xb1b\xfc\xfe\x90\xba3\x103\x99A\xe2\x07\x84\xb3\xd7(\xd1\xab'
As you can see, it's a lot of blocks. Some of the tags aren't even defined in the documentation. The important ones are SK_R1
, R2
, and AR_SEED
for reasons I'll discuss later. So what does a license server do with this information and how does it form a CKC? Before we talk about that, you may notice that there is no asymmetric public key or signature anywhere in the SPC payload, which is where FPS's interesting security theory comes in. Besides encrypting the SPC payload against the application cert, there is no asymmetric crypto involved in FPS. This could be considered a flaw, but it all depends on what FPS did to replace the need for asymmetric crypto. The documentation outlines how a license server processes an SPC and forms a CKC. What it does is it parses the SPC for the SK_R1
block and the AR_SEED
block. The SK_R1
block in an SPC contains the SK
(session key) and R1
(random 1) values, hence the name SK_R1
. However, it's encrypted. If it weren't, it would be far too easy to break FPS.
Two things aren't provided in the FPS SDK: The "D Function" and an example ASk
(application secret key). Those are provided in the deployment package which is only given to partners. Those two things are the core of FPS security. The SK_R1
block is encrypted against a key called the DASk
(derived application secret key) which is computed from the ASk
using the D Function. Now, this is where I'm stumped. How does the client get the ASk
? It needs it to derive the DASk
used for SK_R1
encryption. My theory is that it's in the application certificate, as there are two unknown ASN.1 tags with variable lengths. Figuring out DASk
and ASk
derivation are the only two things that prevent me from making a fully fledged FPS client. Well, I lied. I implemented DASk
derivation since a kind soul leaked the function from the deployment package onto GitHub. It's still useless without ASk
derivation, though. For testing, Apple provides an example DASk
that the example SPCs are encrypted against, which is good. The example DASk
is d87ce7a26081de2e8eb8acef3a6dc179
. For completion, though, I'll outline DASk
derivation. There's a tag in the SPC called R2
, which is a random 21-byte value and the base for DASk
derivation. A custom hash is calculated from the R2
value, and then the resulting hash is encrypted AES ECB using the ASk
as the AES key. The resulting encrypted bytes are the DASk
. Here's an implementation of the R2 hash function in Python:
def DeriveR2Hash(R2):
pad = bytearray(64)
PRIME = 813416437
if len(R2) == 0:
return 0
for i in range(len(R2)):
pad[i] = R2[i]
pad[len(R2)] = 0x80
MBlock = []
for i in range(14):
MBlock.append(
(pad[4 * i] << 24) ^
(pad[4 * i + 1] << 16) ^
(pad[4 * i + 2] << 8) ^
(pad[4 * i + 3])
)
for i in range(1, 7):
MBlock[0] = uint32(MBlock[0])
MBlock[0] += MBlock[i]
MBlock[1] = 0
for i in range(7):
MBlock[1] = uint32(MBlock[1])
MBlock[1] += MBlock[i+7]
for i in range(2):
for r in range(16):
if MBlock[i] & 1 == 1:
MBlock[i] >>= 1
else:
MBlock[i] = (3 * MBlock[i] + 1) % PRIME
for i in range(4):
pad[56+i] = uint8(MBlock[0] >> (8 * i))
pad[60+i] = uint8(MBlock[1] >> (8 * i))
return hashlib.sha1(bytes(pad)).digest()[:16]
Very complicated. FPS's security lies not in asymmetric crypto (well, partly since SPC is encrypted asymmetrically), but in derivation. I think this is so because it is mathematically impossible to white-box an asymmetric key. If you are unaware, white-boxing is a concept in which a symmetric key is obfuscated to the point where an attacker who has full control over the system they are trying to reverse engineer cannot deobfuscate. Note that it can only be applied to symmetric keys. By keeping everything symmetric, FPS can safely rely on white-boxing to obfuscate important keys, which is a brilliant tactic and I do laud them for it. However, if one were to figure out the ASk
and DASk
derivation, FPS's security would be broken. That's why it's both risky and rewarding. FPS doesn't have to worry about the near-impossible task of obfuscating an asymmetric private key, but they do have to worry about keeping their derivation functions obfuscated. With that out of the way, let's decrypt the SK_R1
block and look at its structure. The encrypted SK_R1
TLLV block has a special structure, slightly different than other TLLV blocks. Its struct is as follows:
SK_R1 = Struct(
'tag_name' / Computed('SK_R1'),
'block_length' / Int32ub,
'value_length' / Int32ub,
'iv' / Bytes(16),
'payload' / Bytes(96),
'padding' / Bytes(this.block_length - this.value_length)
)
The SK_R1
block is encrypted AES CBC with the DASk
as the AES key and the IV as the iv
value in the struct. After decrypting the payload, you have another struct. The decrypted SK_R1
payload struct is:
DECRYPTED_SK_R1 = Struct(
'SK' / Bytes(16),
'HU' / Bytes(20),
'R1' / Bytes(44),
'integrity' / Bytes(16)
)
The SK
value is used for encrypting the content keys in a CKC. The HU
value is the device ID of the device that made the SPC. The R1
value is used in deriving the CKC encryption/decryption key. The integrity
value is used as a pseudo-signature for verifying the integrity of the SPC message as a whole. Remember how I said the SK_R1
, R2
, and AR_SEED
TLLV blocks are important? Now that you know the purpose of the SK_R1
and R2
blocks let's go over how a CKC is structured and encrypted.
CKC = Struct(
'version' / Int32ub,
'reserved' / Int32ub,
'iv' / Bytes(16),
'payload_length' / Int32ub,
'payload' / Bytes(this.payload_length)
)
The CKC is encrypted against an AES CBC key derived from the AR_SEED
and R1
blocks from the SPC. Apple provides an example CKCs paired with the example SPCs so that we can decrypt the CKC. Before we do that, I'll go over CKC encryption key derivation, which is documented. Here's a small Python snippet I wrote for CKC decryption:
ar_key = hashlib.sha1(parsed_sk_r1_block.R1).digest()[:16]
aes_cipher = AES.new(ar_key, AES.MODE_ECB)
ckc_key = aes_cipher.encrypt(ar_seed)
aes_cipher = AES.new(ckc_key, AES.MODE_CBC, iv=license.iv)
decrypted_payload = aes_cipher.decrypt(license.payload)
ckc_blocks = TLLV.parse(decrypted_payload)
ar_seed
is the value of the AR_SEED
TLLV block from the SPC. As you can see, the derivation is done by first truncating the SHA1 hash of the R1 value from the decrypted SK_R1
payload to 16 bytes, and then encrypting the ar_seed
with that resulting hash in AES ECB. The resulting encrypted bytes are the key used to encrypt/decrypt the CKC payload. The decrypted CKC payload is also a bunch of TLLV blocks. Here's a parsed decrypted CKC payload:
ListContainer:
Container:
tag = b'X\xb3\x81e\xaf\x0e=Z'
tag_name = ENCRYPTED_CK
block_length = 32
value_length = 32
iv = b'\xd5\xfb\xd6\xb8.\xd9>N\xf9\x8a\xe4\t1\xee3\xb7'
encrypted_ck = b'=VC\x97\x87\x8bpC\xe1T1\xf1\xf8k\xc5b'
padding = b''
Container:
tag = b'\xeat\xc4d]^\xfe\xe9'
tag_name = unknown
block_length = 64
value_length = 44
value = b"'R\x00\x8e\x1c\x11\xe2$\xe8\xeb\x07\xee\xc4\xa0\x9d\x17D\ncr\xd5\xdc!\t\xe5P\xec\xac\x98`a?\x8bz\x8b\xe6\xb4Zi\x83-\x9e\x8c\xe7"
padding = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
Container:
tag = b'\xe6\xd0\xa3\xe7,\x92\xa4\xef'
tag_name = unknown
block_length = 16
value_length = 8
value = b'\x00\x00\x00\x00\x00\x00\x00\x01'
padding = b'\x00\x00\x00\x00\x00\x00\x00\x00'
Container:
tag = b'\x1b\xf7\xf5?]]Z\x1f'
tag_name = ASSET_ID
block_length = 128
value_length = 18
value = b'\xaa\xbb\xcc\xdd\xee\xff\xaa\xbb\xcc\xdd\xee\xff\xaa\xbb\xcc\xdd\xee\xff'
padding = b"rU6>)\xd1RSE\xab\xf4\xc5\x88\xa1\xf7\xa4\x02\xa5w\x17\xe1\x96\xae\xc84\xf2\x93=\xb5\xfc$X\xc7\x94\x1b\xbeso\xa9\xda\xa27\xd21#\x8f\xf8\xe2\x13\x9f\xbcy\xbb\xd3\xf5u]i0\xa6\r\x8ahmk\x11\xef\xacg\x0f\xd0\xf0\x12\xa05\xbd\xba\x08\xb1\x86\x8b(\xc6'\xa8\x83T\xb4Z\xbfAxO\xb22\x1ea\x85\xe6\xfaV\x8cGn\x90\xaf\xb9\x15\xb6\xff"
Container:
tag = b'G\xaaz\xd3D\x05w\xde'
tag_name = TRANSACTION_ID
block_length = 112
value_length = 8
value = b'\x14s\xe5\xccS\xe1\xe5\xd6'
padding = b'\x01x\xe7\xe8\x9b\xc6[y\xe7\xf9-&\x11\xbdKW\x91Z4[O\x07\xd3\xcb\xc60\xf6\xd5o7l\xe0\xfb\xc6\x05\xc9\x93\x1a\x0c\x91\xe4\x00\x01\xf7\xde\x0cr-R\xe8M\xc6\x86\x82\x94x;I\xa9\xfa\x98G\x94\x04!\x8d1\xd0\n\xeb\xbd2\x10\xab/\x0cO\xb9\x1cf\x08Ev\xa6\x0fe\x08s\xbf\xd1\x17\xa51\xe2M\xe8\xa1\xd7\x8fE\x1b\x85\xb6\x1f'
Container:
tag = b'\xf9\x11\xf0M\xa5K\xf5\x99'
tag_name = unknown
block_length = 128
value_length = 121
value = b'\xd9\xa2\xd8N\xbf\x8e\x07\xfaO\x890&\x1f\xf9dcCvk|\xe0\\8\x88\x89\xa8\xed\xd8\rKCRj#\x93)\x1ec\x03\xef\x14\x8a\x13/-\xdex\xcdt\x073\xb7\xb0\r\\wPY\xd6\x8b51\xbeU@0\xbe\x013\x84\xff\x122\xc84\xb1\xa3\xe6\x17\x9bW\x0bn\x9e\x1d\x86\x9fu\xe3\xd1\xe6[\x10E\x1ae`\xea\xd4\xf7\xa2v\xee\xe4\x9e\xf2\xff\x0e\xefMa\x1e+\x8d\x9dnpf\xf1Ms'
padding = b'r\x0fr\\\x1a\x08G'
Container:
tag = b'\xba\x08\xcct\xda\xc9\x17m'
tag_name = unknown
block_length = 32
value_length = 25
value = b'\xfbJv\x13\xe3\xf8H;\xd0\x1e\x1a\xae\x7f\\\xe0\x90,\x00\xc8\xcbg[b$\x85'
padding = b'I\xd4\xe2\x12gh\xea'
Container:
tag = b'\x13\r\x99L\xb8\x94\xb9\xe3'
tag_name = unknown
block_length = 32
value_length = 19
value = b'\xa5\x13T\xe5\x1a\xa0[R\xdf\x0ck\xa5\x0f\x94\x8d\\e\xf56'
padding = b'\xe2\xddh\xd085\xcd/\xabj\xbdU\xfe'
Container:
tag = b'f\xc8#\xf3y\xb8{\xb5'
tag_name = unknown
block_length = 48
value_length = 6
value = b'\xc9\x9f\xb9\x1d%}'
padding = b'\xdf\xb6\x80\x98\xd5z\xe2\xe0\xc9\xa5 m\x1bU\xb4\x9f\xe5\xd8D\x97_\x1dL\xee\x96\xd4dc\xe4\xe4\xadM\x0f\x9byR\x83\xa4\xf1w\xf3+'
Container:
tag = b'\x18\xd4,_\x8eTZK'
tag_name = unknown
block_length = 16
value_length = 6
value = b'\x96\xf8\xb2\xc4\xf7\xa4'
padding = b'&\n\x13$6\xf0\x93\xa7\x054'
The only one of importance is the ENCRYPTED_CK
block, which, as you can probably guess, contains the encrypted content key. The rest of the blocks are blocks from the SPC untouched, as the documentation states that a KSM must include all SPC blocks in a CKC. Decrypting the content key is trivial. All you have to do is decrypt the ENCRYPTED_CK
block value AES ECB with the SPC session key as the AES key. The resulting bytes are the content key. Funnily enough, the content key in the example SPC and CKC Apple provides is 3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b
.
Now you know all about FPS! Except ASk
derivation, of course. I may cover it in the future, and when I do, I'd be able to write my own FPS client that can generate valid SPCs and derive content keys. Thanks for reading!