1use ed25519_dalek::{Signer, SigningKey};
36use hap_tlv8::{Tlv8Map, Tlv8Writer};
37
38use crate::aead::{decrypt, encrypt, hap_nonce};
39use crate::error::{CryptoError, Result};
40use crate::kdf::hkdf_sha512;
41use crate::keys::{verify_ed25519, ControllerKeypair};
42use crate::pair_setup::AccessoryPairing;
43use crate::tlv_types as tlv;
44use crate::x25519::EphemeralKeypair;
45
46const PV_ENCRYPT_SALT: &[u8] = b"Pair-Verify-Encrypt-Salt";
49const PV_ENCRYPT_INFO: &[u8] = b"Pair-Verify-Encrypt-Info";
51const CONTROL_SALT: &[u8] = b"Control-Salt";
53const CONTROL_READ_INFO: &[u8] = b"Control-Read-Encryption-Key";
55const CONTROL_WRITE_INFO: &[u8] = b"Control-Write-Encryption-Key";
57
58const NONCE_M2: &[u8] = b"PV-Msg02";
60const NONCE_M3: &[u8] = b"PV-Msg03";
62
63#[derive(Clone, PartialEq, Eq)]
69pub struct SessionKeys {
70 pub read_key: [u8; 32],
72 pub write_key: [u8; 32],
74}
75
76impl core::fmt::Debug for SessionKeys {
78 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
79 f.debug_struct("SessionKeys").finish_non_exhaustive()
80 }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
85pub enum PairVerifyStep {
86 Send(Vec<u8>),
88 Done(SessionKeys),
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94enum State {
95 Init,
97 AwaitM2,
99 AwaitM4,
101 Done,
103}
104
105pub struct PairVerifyClient {
113 controller_id: String,
114 signing: SigningKey,
115 accessory: AccessoryPairing,
116 ephemeral: EphemeralKeypair,
117 shared_secret: Option<[u8; 32]>,
118 state: State,
119}
120
121impl PairVerifyClient {
122 #[must_use]
125 pub fn new(controller: &ControllerKeypair, accessory: &AccessoryPairing) -> Self {
126 Self::build(controller, accessory, EphemeralKeypair::generate())
127 }
128
129 #[must_use]
135 pub fn new_with_ephemeral(
136 controller: &ControllerKeypair,
137 accessory: &AccessoryPairing,
138 ephemeral_secret: [u8; 32],
139 ) -> Self {
140 Self::build(
141 controller,
142 accessory,
143 EphemeralKeypair::from_secret(ephemeral_secret),
144 )
145 }
146
147 fn build(
148 controller: &ControllerKeypair,
149 accessory: &AccessoryPairing,
150 ephemeral: EphemeralKeypair,
151 ) -> Self {
152 Self {
153 controller_id: controller.id.clone(),
154 signing: controller.signing_key(),
155 accessory: accessory.clone(),
156 ephemeral,
157 shared_secret: None,
158 state: State::Init,
159 }
160 }
161
162 pub fn start(&mut self) -> Vec<u8> {
165 let mut out = Vec::new();
166 let mut w = Tlv8Writer::new(&mut out);
167 w.push_u8(tlv::STATE, tlv::STATE_M1);
168 w.push(tlv::PUBLIC_KEY, &self.ephemeral.public());
169 self.state = State::AwaitM2;
170 out
171 }
172
173 pub fn handle(&mut self, response: &[u8]) -> Result<PairVerifyStep> {
185 match self.state {
186 State::Init => Err(CryptoError::Encoding(
187 "Pair Verify handle called before start",
188 )),
189 State::AwaitM2 => self.handle_m2(response),
190 State::AwaitM4 => self.handle_m4(response),
191 State::Done => Err(CryptoError::Encoding(
192 "Pair Verify handle called after completion",
193 )),
194 }
195 }
196
197 fn handle_m2(&mut self, response: &[u8]) -> Result<PairVerifyStep> {
201 let map = Tlv8Map::parse(response)?;
202 check_error(&map)?;
203 expect_state(&map, tlv::STATE_M2)?;
204
205 let accessory_eph_pub: [u8; 32] = map
206 .get(tlv::PUBLIC_KEY)
207 .ok_or(CryptoError::Encoding("M2 missing accessory ephemeral key"))?
208 .try_into()
209 .map_err(|_| CryptoError::Encoding("M2 accessory ephemeral key not 32 bytes"))?;
210 let encrypted = map
211 .get(tlv::ENCRYPTED_DATA)
212 .ok_or(CryptoError::Encoding("M2 missing encrypted data"))?;
213
214 let controller_eph_pub = self.ephemeral.public();
215 let shared = self.ephemeral.diffie_hellman(&accessory_eph_pub);
216
217 let pv_key = derive_key(&shared, PV_ENCRYPT_SALT, PV_ENCRYPT_INFO)?;
218 let nonce = hap_nonce(NONCE_M2);
219 let plaintext = decrypt(&pv_key, &nonce, b"", encrypted)?;
220
221 let sub = Tlv8Map::parse(&plaintext)?;
223 let identifier = sub
224 .get(tlv::IDENTIFIER)
225 .ok_or(CryptoError::Encoding("M2 sub-TLV missing identifier"))?;
226 let signature: [u8; 64] = sub
227 .get(tlv::SIGNATURE)
228 .ok_or(CryptoError::Encoding("M2 sub-TLV missing signature"))?
229 .try_into()
230 .map_err(|_| CryptoError::Encoding("M2 signature not 64 bytes"))?;
231
232 if identifier != self.accessory.pairing_id.as_bytes() {
234 return Err(CryptoError::Encoding(
235 "M2 accessory identifier does not match stored pairing",
236 ));
237 }
238
239 let mut signed = Vec::with_capacity(32 + identifier.len() + 32);
242 signed.extend_from_slice(&accessory_eph_pub);
243 signed.extend_from_slice(identifier);
244 signed.extend_from_slice(&controller_eph_pub);
245 verify_ed25519(&self.accessory.ltpk, &signed, &signature)?;
246
247 let m3 = self.build_m3(&pv_key, &controller_eph_pub, &accessory_eph_pub)?;
249
250 self.shared_secret = Some(shared);
251 self.state = State::AwaitM4;
252 Ok(PairVerifyStep::Send(m3))
253 }
254
255 fn build_m3(
260 &self,
261 pv_key: &[u8; 32],
262 controller_eph_pub: &[u8; 32],
263 accessory_eph_pub: &[u8; 32],
264 ) -> Result<Vec<u8>> {
265 let id = self.controller_id.as_bytes();
266
267 let mut signed = Vec::with_capacity(32 + id.len() + 32);
268 signed.extend_from_slice(controller_eph_pub);
269 signed.extend_from_slice(id);
270 signed.extend_from_slice(accessory_eph_pub);
271 let signature: [u8; 64] = self.signing.sign(&signed).to_bytes();
272
273 let mut sub = Vec::new();
274 let mut sw = Tlv8Writer::new(&mut sub);
275 sw.push(tlv::IDENTIFIER, id);
276 sw.push(tlv::SIGNATURE, &signature);
277
278 let nonce = hap_nonce(NONCE_M3);
279 let sealed = encrypt(pv_key, &nonce, b"", &sub)?;
280
281 let mut out = Vec::new();
282 let mut w = Tlv8Writer::new(&mut out);
283 w.push_u8(tlv::STATE, tlv::STATE_M3);
284 w.push(tlv::ENCRYPTED_DATA, &sealed);
285 Ok(out)
286 }
287
288 fn handle_m4(&mut self, response: &[u8]) -> Result<PairVerifyStep> {
291 let map = Tlv8Map::parse(response)?;
292 check_error(&map)?;
293 expect_state(&map, tlv::STATE_M4)?;
294
295 let shared = self
296 .shared_secret
297 .ok_or(CryptoError::Encoding("Pair Verify shared secret missing"))?;
298 let read_key = derive_key(&shared, CONTROL_SALT, CONTROL_READ_INFO)?;
299 let write_key = derive_key(&shared, CONTROL_SALT, CONTROL_WRITE_INFO)?;
300
301 self.state = State::Done;
302 Ok(PairVerifyStep::Done(SessionKeys {
303 read_key,
304 write_key,
305 }))
306 }
307}
308
309fn derive_key(shared: &[u8; 32], salt: &[u8], info: &[u8]) -> Result<[u8; 32]> {
311 let mut out = [0u8; 32];
312 hkdf_sha512(shared, salt, info, &mut out)?;
313 Ok(out)
314}
315
316fn check_error(map: &Tlv8Map) -> Result<()> {
318 match map.get(tlv::ERROR) {
319 None | Some([]) => Ok(()),
320 Some(_) => Err(CryptoError::Encoding(
321 "accessory returned a Pair Verify error",
322 )),
323 }
324}
325
326fn expect_state(map: &Tlv8Map, expected: u8) -> Result<()> {
328 match map.get_u8(tlv::STATE)? {
329 None => Ok(()),
331 Some(s) if s == expected => Ok(()),
332 Some(_) => Err(CryptoError::Encoding("unexpected Pair Verify state")),
333 }
334}
335
336#[cfg(test)]
337#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
341mod tests {
342 use super::*;
343 use std::fs;
344 use std::path::PathBuf;
345
346 fn vec_dir() -> PathBuf {
350 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
351 .join("../..")
352 .join("test-vectors/pair-verify")
353 }
354
355 fn load(name: &str) -> Option<Vec<u8>> {
356 fs::read(vec_dir().join(name)).ok()
357 }
358
359 fn load32(name: &str) -> Option<[u8; 32]> {
360 load(name).and_then(|v| v.try_into().ok())
361 }
362
363 fn test_controller() -> ControllerKeypair {
366 ControllerKeypair::from_seed("ABCDEF01-2345-6789".to_string(), [7u8; 32])
367 }
368
369 fn accessory_from_fixtures() -> Option<AccessoryPairing> {
370 let id = String::from_utf8(load("accessory_id.txt")?)
371 .ok()?
372 .trim()
373 .to_string();
374 let ltpk = load32("accessory_ltpk.bin")?;
375 Some(AccessoryPairing {
376 pairing_id: id,
377 ltpk,
378 })
379 }
380
381 #[test]
383 fn m1_reproduces_captured() {
384 let (Some(accessory), Some(eph_priv), Some(m1)) = (
385 accessory_from_fixtures(),
386 load32("ios_eph_priv.bin"),
387 load("m1.bin"),
388 ) else {
389 eprintln!("skipping m1_reproduces_captured: fixtures absent");
390 return;
391 };
392 let mut client =
393 PairVerifyClient::new_with_ephemeral(&test_controller(), &accessory, eph_priv);
394 assert_eq!(client.start(), m1);
395 }
396
397 #[test]
399 fn x25519_matches_captured_shared_secret() {
400 let (Some(eph_priv), Some(m2), Some(shared)) = (
401 load32("ios_eph_priv.bin"),
402 load("m2.bin"),
403 load32("shared_secret.bin"),
404 ) else {
405 eprintln!("skipping x25519_matches_captured_shared_secret: fixtures absent");
406 return;
407 };
408 let map = Tlv8Map::parse(&m2).unwrap();
409 let accessory_eph: [u8; 32] = map.get(tlv::PUBLIC_KEY).unwrap().try_into().unwrap();
410 let kp = EphemeralKeypair::from_secret(eph_priv);
411 assert_eq!(kp.diffie_hellman(&accessory_eph), shared);
412 assert_eq!(
414 crate::x25519::x25519_shared(&eph_priv, &accessory_eph),
415 shared
416 );
417 }
418
419 #[test]
421 fn session_keys_match_captured() {
422 let (Some(shared), Some(read), Some(write)) = (
423 load32("shared_secret.bin"),
424 load32("control_read_encryption_key.bin"),
425 load32("control_write_encryption_key.bin"),
426 ) else {
427 eprintln!("skipping session_keys_match_captured: fixtures absent");
428 return;
429 };
430 assert_eq!(
431 derive_key(&shared, CONTROL_SALT, CONTROL_READ_INFO).unwrap(),
432 read
433 );
434 assert_eq!(
435 derive_key(&shared, CONTROL_SALT, CONTROL_WRITE_INFO).unwrap(),
436 write
437 );
438 }
439
440 #[test]
444 fn full_replay_reaches_done_with_matching_keys() {
445 let (Some(accessory), Some(eph_priv), Some(m2), Some(m4), Some(read), Some(write)) = (
446 accessory_from_fixtures(),
447 load32("ios_eph_priv.bin"),
448 load("m2.bin"),
449 load("m4.bin"),
450 load32("control_read_encryption_key.bin"),
451 load32("control_write_encryption_key.bin"),
452 ) else {
453 eprintln!("skipping full_replay_reaches_done_with_matching_keys: fixtures absent");
454 return;
455 };
456
457 let mut client =
458 PairVerifyClient::new_with_ephemeral(&test_controller(), &accessory, eph_priv);
459 let _m1 = client.start();
460
461 let step = client.handle(&m2).unwrap();
463 let PairVerifyStep::Send(m3) = step else {
464 panic!("expected Send(m3) after M2, got {step:?}");
465 };
466 let m3map = Tlv8Map::parse(&m3).unwrap();
469 assert_eq!(m3map.get_u8(tlv::STATE).unwrap(), Some(tlv::STATE_M3));
470 assert!(m3map.get(tlv::ENCRYPTED_DATA).is_some());
471
472 let done = client.handle(&m4).unwrap();
474 let PairVerifyStep::Done(keys) = done else {
475 panic!("expected Done(SessionKeys) after M4, got {done:?}");
476 };
477 assert_eq!(keys.read_key, read);
478 assert_eq!(keys.write_key, write);
479 }
480
481 #[test]
484 fn corrupt_m2_encrypted_data_errors() {
485 let (Some(accessory), Some(eph_priv), Some(m2)) = (
486 accessory_from_fixtures(),
487 load32("ios_eph_priv.bin"),
488 load("m2.bin"),
489 ) else {
490 eprintln!("skipping corrupt_m2_encrypted_data_errors: fixtures absent");
491 return;
492 };
493
494 let map = Tlv8Map::parse(&m2).unwrap();
497 let accessory_eph = map.get(tlv::PUBLIC_KEY).unwrap().to_vec();
498 let mut enc = map.get(tlv::ENCRYPTED_DATA).unwrap().to_vec();
499 enc[0] ^= 0x01;
500
501 let mut tampered = Vec::new();
502 let mut w = Tlv8Writer::new(&mut tampered);
503 w.push_u8(tlv::STATE, tlv::STATE_M2);
504 w.push(tlv::PUBLIC_KEY, &accessory_eph);
505 w.push(tlv::ENCRYPTED_DATA, &enc);
506
507 let mut client =
508 PairVerifyClient::new_with_ephemeral(&test_controller(), &accessory, eph_priv);
509 let _m1 = client.start();
510 let err = client.handle(&tampered);
511 assert!(
512 matches!(err, Err(CryptoError::Aead | CryptoError::Signature)),
513 "expected Aead/Signature error, got {err:?}"
514 );
515 }
516
517 #[test]
519 fn handle_before_start_errors() {
520 let accessory = AccessoryPairing {
521 pairing_id: "AA:BB:CC:DD:EE:FF".to_string(),
522 ltpk: [0u8; 32],
523 };
524 let mut client = PairVerifyClient::new(&test_controller(), &accessory);
525 assert!(client.handle(b"\x06\x01\x02").is_err());
526 }
527
528 #[test]
530 fn accessory_error_in_m4_errors() {
531 let (Some(accessory), Some(eph_priv), Some(m2)) = (
532 accessory_from_fixtures(),
533 load32("ios_eph_priv.bin"),
534 load("m2.bin"),
535 ) else {
536 eprintln!("skipping accessory_error_in_m4_errors: fixtures absent");
537 return;
538 };
539 let mut client =
540 PairVerifyClient::new_with_ephemeral(&test_controller(), &accessory, eph_priv);
541 let _m1 = client.start();
542 client.handle(&m2).unwrap();
543 let mut m4err = Vec::new();
545 let mut w = Tlv8Writer::new(&mut m4err);
546 w.push_u8(tlv::STATE, tlv::STATE_M4);
547 w.push_u8(tlv::ERROR, 2);
548 assert!(client.handle(&m4err).is_err());
549 }
550}