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 pub fn broadcast_key(&self, controller_ltpk: &[u8]) -> Result<crate::BroadcastKey> {
297 let shared = self
298 .shared_secret
299 .ok_or(CryptoError::Encoding("Pair Verify shared secret missing"))?;
300 crate::BroadcastKey::derive(&shared, controller_ltpk)
301 }
302
303 fn handle_m4(&mut self, response: &[u8]) -> Result<PairVerifyStep> {
306 let map = Tlv8Map::parse(response)?;
307 check_error(&map)?;
308 expect_state(&map, tlv::STATE_M4)?;
309
310 let shared = self
311 .shared_secret
312 .ok_or(CryptoError::Encoding("Pair Verify shared secret missing"))?;
313 let read_key = derive_key(&shared, CONTROL_SALT, CONTROL_READ_INFO)?;
314 let write_key = derive_key(&shared, CONTROL_SALT, CONTROL_WRITE_INFO)?;
315
316 self.state = State::Done;
317 Ok(PairVerifyStep::Done(SessionKeys {
318 read_key,
319 write_key,
320 }))
321 }
322}
323
324fn derive_key(shared: &[u8; 32], salt: &[u8], info: &[u8]) -> Result<[u8; 32]> {
326 let mut out = [0u8; 32];
327 hkdf_sha512(shared, salt, info, &mut out)?;
328 Ok(out)
329}
330
331fn check_error(map: &Tlv8Map) -> Result<()> {
333 match map.get(tlv::ERROR) {
334 None | Some([]) => Ok(()),
335 Some(_) => Err(CryptoError::Encoding(
336 "accessory returned a Pair Verify error",
337 )),
338 }
339}
340
341fn expect_state(map: &Tlv8Map, expected: u8) -> Result<()> {
343 match map.get_u8(tlv::STATE)? {
344 None => Ok(()),
346 Some(s) if s == expected => Ok(()),
347 Some(_) => Err(CryptoError::Encoding("unexpected Pair Verify state")),
348 }
349}
350
351#[cfg(test)]
352#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
356mod tests {
357 use super::*;
358 use std::fs;
359 use std::path::PathBuf;
360
361 fn vec_dir() -> PathBuf {
365 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
366 .join("../..")
367 .join("test-vectors/pair-verify")
368 }
369
370 fn load(name: &str) -> Option<Vec<u8>> {
371 fs::read(vec_dir().join(name)).ok()
372 }
373
374 fn load32(name: &str) -> Option<[u8; 32]> {
375 load(name).and_then(|v| v.try_into().ok())
376 }
377
378 fn test_controller() -> ControllerKeypair {
381 ControllerKeypair::from_seed("ABCDEF01-2345-6789".to_string(), [7u8; 32])
382 }
383
384 fn accessory_from_fixtures() -> Option<AccessoryPairing> {
385 let id = String::from_utf8(load("accessory_id.txt")?)
386 .ok()?
387 .trim()
388 .to_string();
389 let ltpk = load32("accessory_ltpk.bin")?;
390 Some(AccessoryPairing {
391 pairing_id: id,
392 ltpk,
393 })
394 }
395
396 #[test]
398 fn m1_reproduces_captured() {
399 let (Some(accessory), Some(eph_priv), Some(m1)) = (
400 accessory_from_fixtures(),
401 load32("ios_eph_priv.bin"),
402 load("m1.bin"),
403 ) else {
404 eprintln!("skipping m1_reproduces_captured: fixtures absent");
405 return;
406 };
407 let mut client =
408 PairVerifyClient::new_with_ephemeral(&test_controller(), &accessory, eph_priv);
409 assert_eq!(client.start(), m1);
410 }
411
412 #[test]
414 fn x25519_matches_captured_shared_secret() {
415 let (Some(eph_priv), Some(m2), Some(shared)) = (
416 load32("ios_eph_priv.bin"),
417 load("m2.bin"),
418 load32("shared_secret.bin"),
419 ) else {
420 eprintln!("skipping x25519_matches_captured_shared_secret: fixtures absent");
421 return;
422 };
423 let map = Tlv8Map::parse(&m2).unwrap();
424 let accessory_eph: [u8; 32] = map.get(tlv::PUBLIC_KEY).unwrap().try_into().unwrap();
425 let kp = EphemeralKeypair::from_secret(eph_priv);
426 assert_eq!(kp.diffie_hellman(&accessory_eph), shared);
427 assert_eq!(
429 crate::x25519::x25519_shared(&eph_priv, &accessory_eph),
430 shared
431 );
432 }
433
434 #[test]
436 fn session_keys_match_captured() {
437 let (Some(shared), Some(read), Some(write)) = (
438 load32("shared_secret.bin"),
439 load32("control_read_encryption_key.bin"),
440 load32("control_write_encryption_key.bin"),
441 ) else {
442 eprintln!("skipping session_keys_match_captured: fixtures absent");
443 return;
444 };
445 assert_eq!(
446 derive_key(&shared, CONTROL_SALT, CONTROL_READ_INFO).unwrap(),
447 read
448 );
449 assert_eq!(
450 derive_key(&shared, CONTROL_SALT, CONTROL_WRITE_INFO).unwrap(),
451 write
452 );
453 }
454
455 #[test]
459 fn full_replay_reaches_done_with_matching_keys() {
460 let (Some(accessory), Some(eph_priv), Some(m2), Some(m4), Some(read), Some(write)) = (
461 accessory_from_fixtures(),
462 load32("ios_eph_priv.bin"),
463 load("m2.bin"),
464 load("m4.bin"),
465 load32("control_read_encryption_key.bin"),
466 load32("control_write_encryption_key.bin"),
467 ) else {
468 eprintln!("skipping full_replay_reaches_done_with_matching_keys: fixtures absent");
469 return;
470 };
471
472 let mut client =
473 PairVerifyClient::new_with_ephemeral(&test_controller(), &accessory, eph_priv);
474 let _m1 = client.start();
475
476 let step = client.handle(&m2).unwrap();
478 let PairVerifyStep::Send(m3) = step else {
479 panic!("expected Send(m3) after M2, got {step:?}");
480 };
481 let m3map = Tlv8Map::parse(&m3).unwrap();
484 assert_eq!(m3map.get_u8(tlv::STATE).unwrap(), Some(tlv::STATE_M3));
485 assert!(m3map.get(tlv::ENCRYPTED_DATA).is_some());
486
487 let done = client.handle(&m4).unwrap();
489 let PairVerifyStep::Done(keys) = done else {
490 panic!("expected Done(SessionKeys) after M4, got {done:?}");
491 };
492 assert_eq!(keys.read_key, read);
493 assert_eq!(keys.write_key, write);
494 }
495
496 #[test]
499 fn corrupt_m2_encrypted_data_errors() {
500 let (Some(accessory), Some(eph_priv), Some(m2)) = (
501 accessory_from_fixtures(),
502 load32("ios_eph_priv.bin"),
503 load("m2.bin"),
504 ) else {
505 eprintln!("skipping corrupt_m2_encrypted_data_errors: fixtures absent");
506 return;
507 };
508
509 let map = Tlv8Map::parse(&m2).unwrap();
512 let accessory_eph = map.get(tlv::PUBLIC_KEY).unwrap().to_vec();
513 let mut enc = map.get(tlv::ENCRYPTED_DATA).unwrap().to_vec();
514 enc[0] ^= 0x01;
515
516 let mut tampered = Vec::new();
517 let mut w = Tlv8Writer::new(&mut tampered);
518 w.push_u8(tlv::STATE, tlv::STATE_M2);
519 w.push(tlv::PUBLIC_KEY, &accessory_eph);
520 w.push(tlv::ENCRYPTED_DATA, &enc);
521
522 let mut client =
523 PairVerifyClient::new_with_ephemeral(&test_controller(), &accessory, eph_priv);
524 let _m1 = client.start();
525 let err = client.handle(&tampered);
526 assert!(
527 matches!(err, Err(CryptoError::Aead | CryptoError::Signature)),
528 "expected Aead/Signature error, got {err:?}"
529 );
530 }
531
532 #[test]
534 fn handle_before_start_errors() {
535 let accessory = AccessoryPairing {
536 pairing_id: "AA:BB:CC:DD:EE:FF".to_string(),
537 ltpk: [0u8; 32],
538 };
539 let mut client = PairVerifyClient::new(&test_controller(), &accessory);
540 assert!(client.handle(b"\x06\x01\x02").is_err());
541 }
542
543 #[test]
545 fn accessory_error_in_m4_errors() {
546 let (Some(accessory), Some(eph_priv), Some(m2)) = (
547 accessory_from_fixtures(),
548 load32("ios_eph_priv.bin"),
549 load("m2.bin"),
550 ) else {
551 eprintln!("skipping accessory_error_in_m4_errors: fixtures absent");
552 return;
553 };
554 let mut client =
555 PairVerifyClient::new_with_ephemeral(&test_controller(), &accessory, eph_priv);
556 let _m1 = client.start();
557 client.handle(&m2).unwrap();
558 let mut m4err = Vec::new();
560 let mut w = Tlv8Writer::new(&mut m4err);
561 w.push_u8(tlv::STATE, tlv::STATE_M4);
562 w.push_u8(tlv::ERROR, 2);
563 assert!(client.handle(&m4err).is_err());
564 }
565
566 #[test]
569 fn broadcast_key_matches_direct_derive_after_done() {
570 let (Some(accessory), Some(eph_priv), Some(m2), Some(m4)) = (
571 accessory_from_fixtures(),
572 load32("ios_eph_priv.bin"),
573 load("m2.bin"),
574 load("m4.bin"),
575 ) else {
576 eprintln!("skipping broadcast_key_matches_direct_derive_after_done: fixtures absent");
577 return;
578 };
579
580 let controller = test_controller();
581 let mut client = PairVerifyClient::new_with_ephemeral(&controller, &accessory, eph_priv);
582 let _m1 = client.start();
583 client.handle(&m2).unwrap();
584 client.handle(&m4).unwrap();
585
586 let fake_ltpk = [0xABu8; 32];
589 let bk = client.broadcast_key(&fake_ltpk).unwrap();
590
591 let shared = {
594 let map = hap_tlv8::Tlv8Map::parse(&m2).unwrap();
595 let accessory_eph: [u8; 32] = map
596 .get(crate::tlv_types::PUBLIC_KEY)
597 .unwrap()
598 .try_into()
599 .unwrap();
600 crate::x25519::EphemeralKeypair::from_secret(eph_priv).diffie_hellman(&accessory_eph)
601 };
602 let direct = crate::BroadcastKey::derive(&shared, &fake_ltpk).unwrap();
603 assert_eq!(bk.as_bytes(), direct.as_bytes());
604 }
605
606 #[test]
608 fn broadcast_key_before_m2_errors() {
609 let accessory = AccessoryPairing {
610 pairing_id: "AA:BB:CC:DD:EE:FF".to_string(),
611 ltpk: [0u8; 32],
612 };
613 let mut client = PairVerifyClient::new(&test_controller(), &accessory);
614 let _m1 = client.start();
615 assert!(client.broadcast_key(&[0u8; 32]).is_err());
617 }
618}