-
Notifications
You must be signed in to change notification settings - Fork 0
Auth Protocol
Version 0.12
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)
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
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@{... ......….….......................,
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)...................................(
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
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....-..
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
<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`
<v_pub> :`f5078944f29ec2bc3ffe5b04e17772b884ce6d1f88e255582e8b35dda8fa7f35`
<atv_pub> :`d62c8c9548d836736978ad4d426df3495192407bbbb9466c9970794cdd2fe43a`
<atv_data> :`3067a3ea868ade5c9fab43a8d5dc4d53ca1115dbf1c882888f877e85b65c3a82a61583f24c33bf0b9a6ec5c4ab2ecc555a939e7633557453854795e82f2d7ef6`
<shared> :`b7085ca45bd640d966525cbdbc0745bd1d80aa6e6ee48270b60affba3cccac31`
<aes_key> :`2556d9ef1780c8283eecf259fc7207af`
<aes_iv> :`453404da307f780e6d50e52d7dc62325`
<signed> :`82a0cf6cdba66df407fdeb51ac3884748e3a47c8de3f681d534299e707428ce19f6822d2bf925c5d197f1042e7c5b7160a764e42f9fbe33ce57b3704821cff0d`
<signature> :`89dfefdc253147f32f5dc00e4a7042ebccdec663a422c80c1dd5ab69e9cc3304be2de1b0620cdef4749ccdffb4a8f4c4f704124e00f07b6efc3a722f173418a5`
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->new(POST => $url); return if $force; $req->header('Content-Type' => 'application/x-apple-binary-plist'); # there was something strange with UA when set to itunes 12.x.y but can't remember what # $req->header('User-Agent' => '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->new; $req->content($bplist->write($param)); } my $res = $ua->request($req); #say Dumper($res); return undef if !$res->content; my $bplist = Data::Plist::BinaryReader->new; my $data = $bplist->open\_string($res->content)->data; #say Dumper($data); return $data; } my $client\_id = uc(unpack('H\*', Crypt::PRNG::random\_bytes(8))); $client\_id = '366B4165DD64AD3A' if ($force); say ("<ID> :", $client\_id); my $data; my $param; my $ua = LWP::UserAgent->new(keep\_alive => 1); $ua->timeout(2); # step 0) say "step ... 0"; step($ua, '$host/pair-pin-start'); # step 1) say "step ... 1"; $param = { 'method' => 'pin', 'user' => $client\_id}; $data = step($ua, '$host/pair-setup-pin', $param); # step 2) say "step ... 2"; my $client = Crypt::SRP->new('RFC5054-2048bit', 'SHA1'); my $pin; if ($force) { $data->{pk} = Crypt::SRP::\_bignum2bytes(Math::BigInt->new('0x4223ddb35967419ddfece40d6b552b797140129c1c262da1b83d413a7f9674aff834171336dabadf9faa95962331e44838d5f66c46649d583ee44827755651215dcd5881056f7fd7d6445b844ccc5793cc3bbd5887029a5abef8b173a3ad8f81326435e9d49818275734ef483b2541f4e2b99b838164ad5fe4a7cae40599fa41bd0e72cb5495bdd5189805da44b7df9b7ed29af326bb526725c2b1f4115f9d91e41638876eeb1db26ef6aed5373f72e3907cc72997ee9132a0dcafda24115730c9db904acbed6d81dc4b02200a5f5281bf321d5a3216a709191ce6ad36d383e79be76e37a2ed7082007c51717e099e7bedd7387c3f82a916d6aca2eb2b6ff3f3')); $data->{salt} = Crypt::SRP::\_bignum2bytes(Math::BigInt->new('0xd62c98fe76c77ad445828c33063fc36f')); $client->{predefined\_a} = Math::BigInt->new('0xa18b940d3e1302e932a64defccf560a0714b3fa2683bbe3cea808b3abfa58b7d'); $pin = '1234'; } my ($A, $a) = $client->client\_compute\_A(32); my $a\_public = Crypt::Ed25519::eddsa\_public\_key($a); say "<pk> :", unpack("H\*", $data->{pk}); say "<salt> :", unpack("H\*", $data->{salt}); say "<A> :", unpack("H\*", $A); say "<a> :", unpack("H\*", $a); say "<a\_pub> :", unpack("H\*", $a\_public); exit if !$client->client\_verify\_B($data->{pk}); if (!$force) { print "enter PIN: "; $pin = <STDIN>; chomp($pin); } $client->client\_init($client\_id, $pin, $data->{salt}); my $M1 = $client->client\_compute\_M1; say "<M1> :", unpack("H\*", $M1); $param = { 'pk' => $A, 'proof' => $M1 }; $data = step($ua, '$host/pair-setup-pin', $param); #exit if !$client->client\_verify\_M2($data->{proof}); my $K = $client->get\_secret\_K; say "<K> :", unpack("H\*", $K); # step 3) say "step ... 3"; my $sha = Crypt::Digest::SHA512->new; $sha->add( encode('UTF-8','Pair-Setup-AES-Key') ); $sha->add( $K ); my $aes\_key = substr($sha->digest, 0, 16); say "<aes\_key> :", unpack("H\*", $aes\_key); $sha->reset; $sha->add( encode('UTF-8','Pair-Setup-AES-IV') ); $sha->add( $K ); my $aes\_iv = substr($sha->digest, 0, 16); substr($aes\_iv, -1, 1) = pack('C', unpack('C', substr($aes\_iv, -1, 1)) + 1); say "<aes\_iv> :", unpack("H\*", $aes\_iv); my ($epk, $tag) = gcm\_encrypt\_authenticate('AES', $aes\_key, $aes\_iv, '', $a\_public); say "<epk> :", unpack("H\*", $epk); say "<tag> :", unpack("H\*", $tag); $param = { 'epk' => $epk, 'authTag' => $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->new(); my $verify\_secret\_hex; if ($force) { $verify\_secret\_hex = $verifier->secret\_key( unpack('H\*', $a ) ); } else { $verify\_secret\_hex = $verifier->secret\_key( unpack('H\*', Crypt::PRNG::random\_bytes(32)) ); } my $verify\_public = pack("H\*", $verifier->public\_key( $verify\_secret\_hex )); say "verify\_pub :", unpack("H\*", $verify\_public); my $req = HTTP::Request->new(POST => '$host/pair-verify'); $req->header('Content-Type' => 'application/octet-stream'); $req->content("\\x01\\x00\\x00\\x00" . $verify\_public . $a\_public ); my $res = $ua->request($req); #say Dumper($res); $data = $res->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->shared\_secret( $verify\_secret\_hex, unpack("H\*", $atv\_public))); say "shared\_secret :", unpack("H\*", $shared\_secret); my $sha = Crypt::Digest::SHA512->new; $sha->add( encode('UTF-8','Pair-Verify-AES-Key') ); $sha->add( $shared\_secret ); my $aes\_key = substr($sha->digest, 0, 16); say "aes\_key :", unpack("H\*", $aes\_key); $sha->reset; $sha->add( encode('UTF-8','Pair-Verify-AES-IV') ); $sha->add( $shared\_secret ); my $aes\_iv = substr($sha->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->new('AES', 1); $m->start\_encrypt($aes\_key, $aes\_iv); $m->add($atv\_data); my $signature = $m->add($signed); $signature = "\\x00\\x00\\x00\\x00" . $signature; say "signature :", unpack("H\*", $signature); my $req = HTTP::Request->new(POST => '$host/pair-verify'); $req->header('Content-Type' => 'application/octet-stream'); $req->content($signature); my $res = $ua->request($req); if ($res->is\_success) { say "VERIFIED"; } else { say "FAILED"; } ```