Skip to content

Auth Protocol

arag0re.eth edited this page Jan 8, 2023 · 8 revisions

Version 0.12

10. Pairing & Authenticating

Starting with iOS 10.2, AppleTV has forced device authentication. First, the AppleTV must register (pair) the device (only once) and then pairing must be verified at every RTSP connection. Most data are exchanged using Apple’s binary plist format.

It’s critical that the whole exchange (maybe except the /pair-pin-start) is done with the same socket, so make sure your HTTP library uses keep-alive (it is not mandatory, for AppleTV, to send the Connection: keep-alive, just make sure the HTTP tool you use does not close the connection once it has received an answer) . The example below use RTSP/1.0 but HTTP/1.0 seems to be usable with no difference.

10.1. RTSP pairing (required only once)

SETUP START

The pair-pin-start POST request starts the procedure

client → server

POST /pair-pin-start RTSP/1.0
Connection: keep-alive

server → client

RTSP/1.0 200 OK
Server: AirTunes/320.20

SETUP STEP 1 - CONFIRM PIN

The pair-setup-pin POST request is used to display a PIN code on the AppleTV.

Build a plist :

TAG

VALUE

SIZE

DESCRIPTION

user

16

Up to 16 bytes unique ID to describe the device (e.g. MAC address)

method

pin

3

String 'pin' - AppleTV displays a 4 digit pin code

client → server

POST /pair-setup-pin RTSP/1.0
Content-Type: application/x-apple-binary-plist
Content-Length: 86
Connection: keep-alive
bplist00.....VmethodTuserSpin_..00:01:02:03:04:05. ..................................1

The AppleTV response is a plist :

TAG

VALUE

SIZE

DESCRIPTION

pk

256

256 bytes public key from AppleTV

salt

16

Received salt from AppleTV

server → client

RTSP/1.0 200 OK
Content-Length: 342
Content-Type: application/x-apple-binary-plist
Server: AirTunes/320.20
bplist00.....RpkTsaltO....c.Li.4...L.............~....%k#..P2.5...G.U..Y..R..<...r..O....f#.|5ds........Nd..PtSp.g.....S..A..k..c.N...D.B$../....|..^.Y...J^I...h;.
.|6........H.H.8q8....L........]fw....k.....|..7B>....6.z....+.9Es.7(...8E...j.W....U
...f..`.H...HE........onv>f.O......7.;&C..U.z@{... ......….….......................,

SETUP STEP 2 - run S****RP

Generate locally a 32 bytes random auth_secret (named ) that will be used for all further verification with the AppleTV - DO NOT LOOSE IT.

From that secret, create a public key <a_pub> using a Ed25519 ( Edwards-curve Digital Signature Algorithm - EdDSA) – it will be needed later, but can be re-created at any time.

Advice: read SRP6 documentation at http://srp.stanford.edu/design.html to get a better idea of the protocol:

  N    A large safe prime (N = 2q+1, where    from SHA-1, 2048
       q is prime) All arithmetic is done.
       modulo N
  g    A generator modulo N                   “              “
  H()  One-way hash function                  “              “
  k    Multiplier parameter                   calculated by SRP  S    shared secret                          calculated by SRP
  K    shared secret hash                     calculated by SRP (AppleTV special)
  s    User's salt                            received in step 1
  I    Username                               of the client device
  p    Cleartext Password                     as it appears on ATV screen
  ^    (Modular) Exponentiation
  u    Random scrambling parameter            calculated by SRP  a,b  Secret ephemeral values                a = <auth_secret>, A = g^a % N (calculated by SRP)
  A,B  Public ephemeral values                A = calculated by SRP, B = received from AppleTV
  x    Private key (derived from p and s)     calculated by SRP
  v    Password verifier                      calculated by SRP

Note: some SRP libraries seems to generate the proof M1 with

    M1 = H( H(N) ^ H(PAD(g)) | H(I) | s | PAD(A) | PAD(B) | K )

but AppleTV does not want to be padded, so simply modify your tool to do

    M1 = H( H(N) ^ H(g) | H(I) | s | PAD(A) | PAD(B) | K )

(NB : ‘|’ represents a byte array concatenation, not a logical “or” and ^ is XOR)

Note: The calculation of , the shared secret hash is not compliant with default SRP6

    K =H(S)

but AppleTV requires

    K = H(S | \x00\x00\x00\x00) | H(S | \x00\x00\x00\x01)

Now feed the modified SRP library with , , , , and it will give you M1.

Build a plist:

TAG

VALUE

SIZE

DESCRIPTION

pk

256

The SRP6 public key corresponding to (A = g ^ % N).
Should be calculated by the SRP library

proof

20

The SRP6 public key corresponding to (A = g ^ % N).
Should be calculated by the SRP library

client → server

POST /pair-setup-pin RTSP/1.0
Content-Type: application/x-apple-binary-plist
Content-Length: 347
Connection: keep-alive
bplist00.....RpkUproofO.....~.!..S|..5..M..)7..r.?.....j.N..0...[K.uu.. q+..O.0...c.!...\O......*.[k(.6.?Mv..-yS.......;[email protected][email protected].. ..... V.._k..........E.^}0.&...mvwpA=)X.}....OF...JZQ...o.,..(..G.g.{...0....wj,?...........G.m,..... .0.)..S....7...1.Q9PA.ni.d=....<=..}.O..2....d...... ..Z..j.... .....................................1

The AppleTV response is a **plist :
**

TAG

VALUE

SIZE

DESCRIPTION

proof

64

The M2 proof calculated by the server

This can be checked against the calculated by SRP for verification on the client side, It is optional for the client to verify this because if the AppleTV detects an error, an HTTP 470 will be responded.

server → client

RTSP/1.0 200 OK 
Content-Length: 75 
Content-Type: application/x-apple-binary-plist 
Server: AirTunes/320.20 
bplist00...UproofO...G.f.)....A....q.P)...................................(

SETUP STEP 3 (run AES)

This last step is to confirm the secret auth_secret . It requires a sha512 digest, and an AES encryption in GCM (Galois Counter Mode, which provides encryption and signature). This builds an encrypted message with its signed .

  • Create the AES key with the first 16 bytes of a sha512 digest made with
    • The string “Pair-Setup-AES-Key” (in UTF-8)
    • The shared secret hash from SRP6 (see above)
  • Create the AES IV with the first 16 bytes of a sha512 digest made with
    • The string “Pair-Setup-AES-IV” (in UTF-8)
    • The shared secret hash from SRP6 (see above)
    • Add 0x01 to the last byte of the AES IV key
  • Use this AES key and IV to encode the public key <a_pub>created with Ed25519 using AES in GCM
    • The encrypted data is and the signature is

Build a plist:

TAG

VALUE

SIZE

DESCRIPTION

epk

32

Encrypted data

authTag

64

Signature of data

client → server

POST /pair-setup-pin RTSP/1.0
Content-Type: application/x-apple-binary-plist
Content-Length: 116
bplist00.....SepkWauthTagO. .p..^.......vLk.&....&.(A.].....O..K...sW.&#..Q..... ..<...............................O

If the message is correct, the AppleTV should responds with a HTTP 200 and a plist which means that the auth_secret is now registered in the AppleTV as a valid secret

TAG

VALUE

SIZE

DESCRIPTION

epk

32

unused

authTag

64

unused

These parameters do not seem to be used later in the protocol

server → client

RTSP/1.0 200 OK
Content-Length: 116
Content-Type: application/x-apple-binary-plist
Server: AirTunes/320.20
bplist00.....SepkWauthTagO. [email protected].&ED...Q

10.2. RTSP session authentication

Now, before initialing any audio, video or exchange with the AppleTV, each RTSP session must be authenticated. This means that after a TEARDOWN, the same protocol must happen again (only the authentication, not the pairing). The auth_secret will now be used

VERIFY STEP 1 – CREATE NEW KEYS

Generate locally a random 32 bytes number and use a Curve25519 elliptic curve Diffie–Hellman (ECDH) algorithm to build a verifier key pair <v_pub> and <v_priv> (32 bytes each).

As above, retrieve the public key <a_pub> from the auth_secret using Ed25519 (Edwards-curve Digital Signature Algorithm - EdDSA)

Concatenate <v_pub> and <a_pub>, lead by a 4 bytes header “\0x01\0x00\0x00\0x00” and send this in the body of an HTTP POST request (68 bytes):

    body =     “\0x01\0x00\0x00\0x00”<v_pub><a_pub>

This is NOT a plist

client → server

POST /pair-verify RTSP/1.0
Content-Type: application/octet-stream
Content-Length: 68
....\[email protected].>+..qQ.. .>+..qQ..

The AppleTV responds with a body containing 96 bytes of data. Again, this is NOT a plist, but just plain binary. The first 32 bytes are an AppleTV public key <atv_pub> and the remaining bytes (usually 64) are some data <atv_data> to be used in next step.

server → client

RTSP/1.0 200 OK
Content-Length: 96
Content-Type: application/octet-stream
Server: AirTunes/320.20.1
.............9...%....bN.KL9$k....N.........k.)...kI-..s_T...r.......Y.......9.41..h6..g....-..

VERIFY STEP 2 - AES

This last step is to finalize the verification. It requires Ed25519, Curve25519, a sha512 digest, and an AES encryption in CTR (Counter) mode. This provides an encrypted message that will be sent in an HTTP/RTSP body

  • Create a shared secret between <v_priv> and <atv_pub>, using Curve25519 algorithm

  • Use Ed25519 signature algorithm to sign the concatenation of <v_pub> and <atv_pub> (in that order), using the keypair <a_pub> and . Call the result

  • Create the AES key with the first 16 bytes of a sha512 digest made with

    • The string “Pair-Verify-AES-Key” (in UTF-8)
    • The shared secret created on using Curve25519
  • Create the AES IV with the first 16 bytes of a sha512 digest made with

    • The string “Pair-Verify-AES-IV” (in UTF-8)
    • The shared secret created on using Curve25519
  • Use this AES key and IV to build a signature by encoding <atv_data> and . Encrypt <atv_data> first, discard the result and then encrypt . Although the first encrypted data is discarded, this is important due to the counter feature of AES CTR

    • Encrypt <atv_data>, discard result
    • Encrypt and keep result as the signature (64 bytes)
  • Concatenate this with a 4 bytes header “\0x00\0x00\0x00\0x00” and send this in the body of an HTTP POST request (68 bytes):

client → server

POST /pair-verify RTSP/1.0
Content-Type: application/octet-stream
Content-Length: 68
....F.ATu.:.{..............X......."D2.C!....R;.P~....-....4.ZT......

The AppleTV should respond with an HTTP/RTSP 200 and the session (socket) is valid

server → client

RTSP/1.0 200 OK
Content-Length: 0
Content-Type: application/octet-stream
Server: AirTunes/320.20.1

10.3. Test vector

As this is an awful lot of cryptography where everything can and will go wrong, so here is a test vector to verify that you do step by step

PAIRING

<ID>    :`366B4165DD64AD3A` (as a string, not an HEX value)
<pk>    :`4223ddb35967419ddfece40d6b552b797140129c1c262da1b83d413a7f9674aff834171336dabadf9faa95962331e44838d5f66c46649d583ee44827755651215dcd5881056f7fd7d6445b844ccc5793cc3bbd5887029a5abef8b173a3ad8f81326435e9d49818275734ef483b2541f4e2b99b838164ad5fe4a7cae40599fa41bd0e72cb5495bdd5189805da44b7df9b7ed29af326bb526725c2b1f4115f9d91e41638876eeb1db26ef6aed5373f72e3907cc72997ee9132a0dcafda24115730c9db904acbed6d81dc4b02200a5f5281bf321d5a3216a709191ce6ad36d383e79be76e37a2ed7082007c51717e099e7bedd7387c3f82a916d6aca2eb2b6ff3f3`
<salt>  :`d62c98fe76c77ad445828c33063fc36f`
<A>     :`47662731cbe1ba0b130dc5e65320dc2a4b60371e086212a7a55ed4a3653b2d1e861569309c97b4f88433564bd47f6de13ecc440db26998478b266eaa8195a81c28f89a989bc538c477be302fd96bb3fa809e9a94b0aac28d6a00aa057892ba26b2b2cad4d8ec6a9e4207754926c985c393feb6e8b7fb82bd8043709866d7b53a592a940d8e44a7d08fbbda51bf5c9091c251988236147364cb75ad5a4efbeed242fd78496f0cda365965255c8214bd264c259fa2f2a8bfec70eecb32d2ded4c5c35e5e802a22bf58f7cd629fb2f3b4a2498b95f63eab37be9fb0f75c3fcbea8c083d0311302ebc2c3bc0a0525ba5bf3fcffe5b5668b4905a8e6cdb70d89f4b1b`  
<a>     :`a18b940d3e1302e932a64defccf560a0714b3fa2683bbe3cea808b3abfa58b7d`
<a_pub> :`0ceaa63dedd87d2da05ff0bdfbd99b5734911269c70664b9a74e04ae5cdbeca7`
<M1>    :`4b4e638bf08526e4229fd079675fedfd329b97ef`
<K>     :`9a689113a76b44583e73f9662eb172e830886ed988f04c6c0030f0e93c68784de27dbf30c5d151fb`
<pin>   :`1234` (as a string)   
<aes_key>   :`a043357cee40a9ae0731dd50859cccfb`
<aes_iv>    :`da36ea69a94d51d881086e9080dbaef8`   
<epk>       :`5de0f61622b0d41bc098b07f229863f49e1a1c1030908b0ec620386e089a20c4` 
<tag>       :`3b13d2e85f00555c6a05df5cb03a2105`

VERIFYING

<v_pub>     :`f5078944f29ec2bc3ffe5b04e17772b884ce6d1f88e255582e8b35dda8fa7f35`
<atv_pub>   :`d62c8c9548d836736978ad4d426df3495192407bbbb9466c9970794cdd2fe43a`
<atv_data>  :`3067a3ea868ade5c9fab43a8d5dc4d53ca1115dbf1c882888f877e85b65c3a82a61583f24c33bf0b9a6ec5c4ab2ecc555a939e7633557453854795e82f2d7ef6`
<shared>    :`b7085ca45bd640d966525cbdbc0745bd1d80aa6e6ee48270b60affba3cccac31`
<aes_key>   :`2556d9ef1780c8283eecf259fc7207af`
<aes_iv>    :`453404da307f780e6d50e52d7dc62325`
<signed>    :`82a0cf6cdba66df407fdeb51ac3884748e3a47c8de3f681d534299e707428ce19f6822d2bf925c5d197f1042e7c5b7160a764e42f9fbe33ce57b3704821cff0d`
<signature> :`89dfefdc253147f32f5dc00e4a7042ebccdec663a422c80c1dd5ab69e9cc3304be2de1b0620cdef4749ccdffb4a8f4c4f704124e00f07b6efc3a722f173418a5`

10.4. Perl code example

use strict; use strict; use File::Spec::Functions; use Data::Dumper; use LWP::Simple; use LWP; use Data::Plist::BinaryWriter; use Data::Plist::BinaryReader; use feature qw(say); use Crypt::SRP; use Math::BigInt; use Crypt::Digest::SHA512 qw (sha512); use Crypt::AuthEnc::GCM qw(gcm\_encrypt\_authenticate gcm\_decrypt\_verify); use Crypt::Ed25519; use Crypt::Curve25519; use Crypt::Mode::CTR; use Encode qw(decode encode); my $force = 0; my $host = 'http://192.168.1.10:7000'; sub step { my ($ua, $url, $param) = @\_; my $req = HTTP::Request-&gt;new(POST =&gt; $url); return if $force; $req-&gt;header('Content-Type' =&gt; 'application/x-apple-binary-plist'); # there was something strange with UA when set to itunes 12.x.y but can't remember what # $req-&gt;header('User-Agent' =&gt; 'iTunes/4.7.1 (Windows; N; Windows 7; 8664; EN; cp1252) SqueezeCenter, Squeezebox Server, Logitech Media Server'); if (defined $param) { my $bplist = Data::Plist::BinaryWriter-&gt;new; $req-&gt;content($bplist-&gt;write($param)); } my $res = $ua-&gt;request($req); #say Dumper($res); return undef if !$res-&gt;content; my $bplist = Data::Plist::BinaryReader-&gt;new; my $data = $bplist-&gt;open\_string($res-&gt;content)-&gt;data; #say Dumper($data); return $data; } my $client\_id = uc(unpack('H\*', Crypt::PRNG::random\_bytes(8))); $client\_id = '366B4165DD64AD3A' if ($force); say ("&lt;ID&gt; :", $client\_id); my $data; my $param; my $ua = LWP::UserAgent-&gt;new(keep\_alive =&gt; 1); $ua-&gt;timeout(2); # step 0) say "step ... 0"; step($ua, '$host/pair-pin-start'); # step 1) say "step ... 1"; $param = { 'method' =&gt; 'pin', 'user' =&gt; $client\_id}; $data = step($ua, '$host/pair-setup-pin', $param); # step 2) say "step ... 2"; my $client = Crypt::SRP-&gt;new('RFC5054-2048bit', 'SHA1'); my $pin; if ($force) { $data-&gt;{pk} = Crypt::SRP::\_bignum2bytes(Math::BigInt-&gt;new('0x4223ddb35967419ddfece40d6b552b797140129c1c262da1b83d413a7f9674aff834171336dabadf9faa95962331e44838d5f66c46649d583ee44827755651215dcd5881056f7fd7d6445b844ccc5793cc3bbd5887029a5abef8b173a3ad8f81326435e9d49818275734ef483b2541f4e2b99b838164ad5fe4a7cae40599fa41bd0e72cb5495bdd5189805da44b7df9b7ed29af326bb526725c2b1f4115f9d91e41638876eeb1db26ef6aed5373f72e3907cc72997ee9132a0dcafda24115730c9db904acbed6d81dc4b02200a5f5281bf321d5a3216a709191ce6ad36d383e79be76e37a2ed7082007c51717e099e7bedd7387c3f82a916d6aca2eb2b6ff3f3')); $data-&gt;{salt} = Crypt::SRP::\_bignum2bytes(Math::BigInt-&gt;new('0xd62c98fe76c77ad445828c33063fc36f')); $client-&gt;{predefined\_a} = Math::BigInt-&gt;new('0xa18b940d3e1302e932a64defccf560a0714b3fa2683bbe3cea808b3abfa58b7d'); $pin = '1234'; } my ($A, $a) = $client-&gt;client\_compute\_A(32); my $a\_public = Crypt::Ed25519::eddsa\_public\_key($a); say "&lt;pk&gt; :", unpack("H\*", $data-&gt;{pk}); say "&lt;salt&gt; :", unpack("H\*", $data-&gt;{salt}); say "&lt;A&gt; :", unpack("H\*", $A); say "&lt;a&gt; :", unpack("H\*", $a); say "&lt;a\_pub&gt; :", unpack("H\*", $a\_public); exit if !$client-&gt;client\_verify\_B($data-&gt;{pk}); if (!$force) { print "enter PIN: "; $pin = &lt;STDIN&gt;; chomp($pin); } $client-&gt;client\_init($client\_id, $pin, $data-&gt;{salt}); my $M1 = $client-&gt;client\_compute\_M1; say "&lt;M1&gt; :", unpack("H\*", $M1); $param = { 'pk' =&gt; $A, 'proof' =&gt; $M1 }; $data = step($ua, '$host/pair-setup-pin', $param); #exit if !$client-&gt;client\_verify\_M2($data-&gt;{proof}); my $K = $client-&gt;get\_secret\_K; say "&lt;K&gt; :", unpack("H\*", $K); # step 3) say "step ... 3"; my $sha = Crypt::Digest::SHA512-&gt;new; $sha-&gt;add( encode('UTF-8','Pair-Setup-AES-Key') ); $sha-&gt;add( $K ); my $aes\_key = substr($sha-&gt;digest, 0, 16); say "&lt;aes\_key&gt; :", unpack("H\*", $aes\_key); $sha-&gt;reset; $sha-&gt;add( encode('UTF-8','Pair-Setup-AES-IV') ); $sha-&gt;add( $K ); my $aes\_iv = substr($sha-&gt;digest, 0, 16); substr($aes\_iv, -1, 1) = pack('C', unpack('C', substr($aes\_iv, -1, 1)) + 1); say "&lt;aes\_iv&gt; :", unpack("H\*", $aes\_iv); my ($epk, $tag) = gcm\_encrypt\_authenticate('AES', $aes\_key, $aes\_iv, '', $a\_public); say "&lt;epk&gt; :", unpack("H\*", $epk); say "&lt;tag&gt; :", unpack("H\*", $tag); $param = { 'epk' =&gt; $epk, 'authTag' =&gt; $tag }; $data = step($ua, '$host/pair-setup-pin', $param); if (defined $data) { say "SUCCESS"; } else { say "FAILED"; } my $credentials = $client\_id . ":" . unpack("H\*", $a); say "credentials :", $credentials; # ============================= VERIFICATION =================================== # verification 1) my ($client\_id, $a) = split(/:/, $credentials); $a = pack("H\*", $a); my $a\_public = Crypt::Ed25519::eddsa\_public\_key($a); say "a\_pub :", unpack("H\*", $a\_public); say "verify ... 1"; my $verifier = Crypt::Curve25519-&gt;new(); my $verify\_secret\_hex; if ($force) { $verify\_secret\_hex = $verifier-&gt;secret\_key( unpack('H\*', $a ) ); } else { $verify\_secret\_hex = $verifier-&gt;secret\_key( unpack('H\*', Crypt::PRNG::random\_bytes(32)) ); } my $verify\_public = pack("H\*", $verifier-&gt;public\_key( $verify\_secret\_hex )); say "verify\_pub :", unpack("H\*", $verify\_public); my $req = HTTP::Request-&gt;new(POST =&gt; '$host/pair-verify'); $req-&gt;header('Content-Type' =&gt; 'application/octet-stream'); $req-&gt;content("\\x01\\x00\\x00\\x00" . $verify\_public . $a\_public ); my $res = $ua-&gt;request($req); #say Dumper($res); $data = $res-&gt;content; if ($force) { $data = pack("H\*", 'd62c8c9548d836736978ad4d426df3495192407bbbb9466c9970794cdd2fe43a3067a3ea868ade5c9fab43a8d5dc4d53ca1115dbf1c882888f877e85b65c3a82a61583f24c33bf0b9a6ec5c4ab2ecc555a939e7633557453854795e82f2d7ef6'); } my $atv\_public = substr($data, 0, 32); my $atv\_data = substr($data, 32); say "atv\_public :", unpack("H\*", $atv\_public); say "atv\_data :", unpack("H\*", $atv\_data); # verification 2) say "verify ... 2"; my $shared\_secret = pack("H\*", $verifier-&gt;shared\_secret( $verify\_secret\_hex, unpack("H\*", $atv\_public))); say "shared\_secret :", unpack("H\*", $shared\_secret); my $sha = Crypt::Digest::SHA512-&gt;new; $sha-&gt;add( encode('UTF-8','Pair-Verify-AES-Key') ); $sha-&gt;add( $shared\_secret ); my $aes\_key = substr($sha-&gt;digest, 0, 16); say "aes\_key :", unpack("H\*", $aes\_key); $sha-&gt;reset; $sha-&gt;add( encode('UTF-8','Pair-Verify-AES-IV') ); $sha-&gt;add( $shared\_secret ); my $aes\_iv = substr($sha-&gt;digest, 0, 16); say "aes\_iv :", unpack("H\*", $aes\_iv); my $signed = Crypt::Ed25519::eddsa\_sign($verify\_public . $atv\_public, $a\_public, $a); #say "buf: ", unpack("H\*", $verify\_public . $atv\_public); say "signed :", unpack("H\*", $signed); my $m = Crypt::Mode::CTR-&gt;new('AES', 1); $m-&gt;start\_encrypt($aes\_key, $aes\_iv); $m-&gt;add($atv\_data); my $signature = $m-&gt;add($signed); $signature = "\\x00\\x00\\x00\\x00" . $signature; say "signature :", unpack("H\*", $signature); my $req = HTTP::Request-&gt;new(POST =&gt; '$host/pair-verify'); $req-&gt;header('Content-Type' =&gt; 'application/octet-stream'); $req-&gt;content($signature); my $res = $ua-&gt;request($req); if ($res-&gt;is\_success) { say "VERIFIED"; } else { say "FAILED"; } ```