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";
57const EVENT_SALT: &[u8] = b"Event-Salt";
59const EVENT_READ_INFO: &[u8] = b"Event-Read-Encryption-Key";
61
62const NONCE_M2: &[u8] = b"PV-Msg02";
64const NONCE_M3: &[u8] = b"PV-Msg03";
66
67#[derive(Clone, PartialEq, Eq)]
73pub struct SessionKeys {
74 pub read_key: [u8; 32],
76 pub write_key: [u8; 32],
78}
79
80impl core::fmt::Debug for SessionKeys {
82 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
83 f.debug_struct("SessionKeys").finish_non_exhaustive()
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum PairVerifyStep {
90 Send(Vec<u8>),
92 Done(SessionKeys),
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98enum State {
99 Init,
101 AwaitM2,
103 AwaitM4,
105 Done,
107}
108
109pub struct PairVerifyClient {
117 controller_id: String,
118 signing: SigningKey,
119 accessory: AccessoryPairing,
120 ephemeral: EphemeralKeypair,
121 shared_secret: Option<[u8; 32]>,
122 state: State,
123}
124
125impl PairVerifyClient {
126 #[must_use]
129 pub fn new(controller: &ControllerKeypair, accessory: &AccessoryPairing) -> Self {
130 Self::build(controller, accessory, EphemeralKeypair::generate())
131 }
132
133 #[must_use]
139 pub fn new_with_ephemeral(
140 controller: &ControllerKeypair,
141 accessory: &AccessoryPairing,
142 ephemeral_secret: [u8; 32],
143 ) -> Self {
144 Self::build(
145 controller,
146 accessory,
147 EphemeralKeypair::from_secret(ephemeral_secret),
148 )
149 }
150
151 fn build(
152 controller: &ControllerKeypair,
153 accessory: &AccessoryPairing,
154 ephemeral: EphemeralKeypair,
155 ) -> Self {
156 Self {
157 controller_id: controller.id.clone(),
158 signing: controller.signing_key(),
159 accessory: accessory.clone(),
160 ephemeral,
161 shared_secret: None,
162 state: State::Init,
163 }
164 }
165
166 pub fn start(&mut self) -> Vec<u8> {
169 let mut out = Vec::new();
170 let mut w = Tlv8Writer::new(&mut out);
171 w.push_u8(tlv::STATE, tlv::STATE_M1);
172 w.push(tlv::PUBLIC_KEY, &self.ephemeral.public());
173 self.state = State::AwaitM2;
174 out
175 }
176
177 pub fn handle(&mut self, response: &[u8]) -> Result<PairVerifyStep> {
189 match self.state {
190 State::Init => Err(CryptoError::Encoding(
191 "Pair Verify handle called before start",
192 )),
193 State::AwaitM2 => self.handle_m2(response),
194 State::AwaitM4 => self.handle_m4(response),
195 State::Done => Err(CryptoError::Encoding(
196 "Pair Verify handle called after completion",
197 )),
198 }
199 }
200
201 fn handle_m2(&mut self, response: &[u8]) -> Result<PairVerifyStep> {
205 let map = Tlv8Map::parse(response)?;
206 check_error(&map)?;
207 expect_state(&map, tlv::STATE_M2)?;
208
209 let accessory_eph_pub: [u8; 32] = map
210 .get(tlv::PUBLIC_KEY)
211 .ok_or(CryptoError::Encoding("M2 missing accessory ephemeral key"))?
212 .try_into()
213 .map_err(|_| CryptoError::Encoding("M2 accessory ephemeral key not 32 bytes"))?;
214 let encrypted = map
215 .get(tlv::ENCRYPTED_DATA)
216 .ok_or(CryptoError::Encoding("M2 missing encrypted data"))?;
217
218 let controller_eph_pub = self.ephemeral.public();
219 let shared = self.ephemeral.diffie_hellman(&accessory_eph_pub);
220
221 let pv_key = derive_key(&shared, PV_ENCRYPT_SALT, PV_ENCRYPT_INFO)?;
222 let nonce = hap_nonce(NONCE_M2);
223 let plaintext = decrypt(&pv_key, &nonce, b"", encrypted)?;
224
225 let sub = Tlv8Map::parse(&plaintext)?;
227 let identifier = sub
228 .get(tlv::IDENTIFIER)
229 .ok_or(CryptoError::Encoding("M2 sub-TLV missing identifier"))?;
230 let signature: [u8; 64] = sub
231 .get(tlv::SIGNATURE)
232 .ok_or(CryptoError::Encoding("M2 sub-TLV missing signature"))?
233 .try_into()
234 .map_err(|_| CryptoError::Encoding("M2 signature not 64 bytes"))?;
235
236 if identifier != self.accessory.pairing_id.as_bytes() {
238 return Err(CryptoError::Encoding(
239 "M2 accessory identifier does not match stored pairing",
240 ));
241 }
242
243 let mut signed = Vec::with_capacity(32 + identifier.len() + 32);
246 signed.extend_from_slice(&accessory_eph_pub);
247 signed.extend_from_slice(identifier);
248 signed.extend_from_slice(&controller_eph_pub);
249 verify_ed25519(&self.accessory.ltpk, &signed, &signature)?;
250
251 let m3 = self.build_m3(&pv_key, &controller_eph_pub, &accessory_eph_pub)?;
253
254 self.shared_secret = Some(shared);
255 self.state = State::AwaitM4;
256 Ok(PairVerifyStep::Send(m3))
257 }
258
259 fn build_m3(
264 &self,
265 pv_key: &[u8; 32],
266 controller_eph_pub: &[u8; 32],
267 accessory_eph_pub: &[u8; 32],
268 ) -> Result<Vec<u8>> {
269 let id = self.controller_id.as_bytes();
270
271 let mut signed = Vec::with_capacity(32 + id.len() + 32);
272 signed.extend_from_slice(controller_eph_pub);
273 signed.extend_from_slice(id);
274 signed.extend_from_slice(accessory_eph_pub);
275 let signature: [u8; 64] = self.signing.sign(&signed).to_bytes();
276
277 let mut sub = Vec::new();
278 let mut sw = Tlv8Writer::new(&mut sub);
279 sw.push(tlv::IDENTIFIER, id);
280 sw.push(tlv::SIGNATURE, &signature);
281
282 let nonce = hap_nonce(NONCE_M3);
283 let sealed = encrypt(pv_key, &nonce, b"", &sub)?;
284
285 let mut out = Vec::new();
286 let mut w = Tlv8Writer::new(&mut out);
287 w.push_u8(tlv::STATE, tlv::STATE_M3);
288 w.push(tlv::ENCRYPTED_DATA, &sealed);
289 Ok(out)
290 }
291
292 pub fn broadcast_key(&self, controller_ltpk: &[u8]) -> Result<crate::BroadcastKey> {
301 let shared = self
302 .shared_secret
303 .ok_or(CryptoError::Encoding("Pair Verify shared secret missing"))?;
304 crate::BroadcastKey::derive(&shared, controller_ltpk)
305 }
306
307 pub fn event_key(&self) -> Result<[u8; 32]> {
320 let shared = self
321 .shared_secret
322 .ok_or(CryptoError::Encoding("Pair Verify shared secret missing"))?;
323 derive_key(&shared, EVENT_SALT, EVENT_READ_INFO)
324 }
325
326 fn handle_m4(&mut self, response: &[u8]) -> Result<PairVerifyStep> {
329 let map = Tlv8Map::parse(response)?;
330 check_error(&map)?;
331 expect_state(&map, tlv::STATE_M4)?;
332
333 let shared = self
334 .shared_secret
335 .ok_or(CryptoError::Encoding("Pair Verify shared secret missing"))?;
336 let read_key = derive_key(&shared, CONTROL_SALT, CONTROL_READ_INFO)?;
337 let write_key = derive_key(&shared, CONTROL_SALT, CONTROL_WRITE_INFO)?;
338
339 self.state = State::Done;
340 Ok(PairVerifyStep::Done(SessionKeys {
341 read_key,
342 write_key,
343 }))
344 }
345}
346
347fn derive_key(shared: &[u8; 32], salt: &[u8], info: &[u8]) -> Result<[u8; 32]> {
349 let mut out = [0u8; 32];
350 hkdf_sha512(shared, salt, info, &mut out)?;
351 Ok(out)
352}
353
354fn check_error(map: &Tlv8Map) -> Result<()> {
356 match map.get(tlv::ERROR) {
357 None | Some([]) => Ok(()),
358 Some(_) => Err(CryptoError::Encoding(
359 "accessory returned a Pair Verify error",
360 )),
361 }
362}
363
364fn expect_state(map: &Tlv8Map, expected: u8) -> Result<()> {
366 match map.get_u8(tlv::STATE)? {
367 None => Ok(()),
369 Some(s) if s == expected => Ok(()),
370 Some(_) => Err(CryptoError::Encoding("unexpected Pair Verify state")),
371 }
372}
373
374#[cfg(test)]
375#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
379mod tests {
380 use super::*;
381 use std::fs;
382 use std::path::PathBuf;
383
384 fn vec_dir() -> PathBuf {
388 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
389 .join("../..")
390 .join("test-vectors/pair-verify")
391 }
392
393 fn load(name: &str) -> Option<Vec<u8>> {
394 fs::read(vec_dir().join(name)).ok()
395 }
396
397 fn load32(name: &str) -> Option<[u8; 32]> {
398 load(name).and_then(|v| v.try_into().ok())
399 }
400
401 fn test_controller() -> ControllerKeypair {
404 ControllerKeypair::from_seed("ABCDEF01-2345-6789".to_string(), [7u8; 32])
405 }
406
407 fn accessory_from_fixtures() -> Option<AccessoryPairing> {
408 let id = String::from_utf8(load("accessory_id.txt")?)
409 .ok()?
410 .trim()
411 .to_string();
412 let ltpk = load32("accessory_ltpk.bin")?;
413 Some(AccessoryPairing {
414 pairing_id: id,
415 ltpk,
416 })
417 }
418
419 #[test]
421 fn m1_reproduces_captured() {
422 let (Some(accessory), Some(eph_priv), Some(m1)) = (
423 accessory_from_fixtures(),
424 load32("ios_eph_priv.bin"),
425 load("m1.bin"),
426 ) else {
427 eprintln!("skipping m1_reproduces_captured: fixtures absent");
428 return;
429 };
430 let mut client =
431 PairVerifyClient::new_with_ephemeral(&test_controller(), &accessory, eph_priv);
432 assert_eq!(client.start(), m1);
433 }
434
435 #[test]
437 fn x25519_matches_captured_shared_secret() {
438 let (Some(eph_priv), Some(m2), Some(shared)) = (
439 load32("ios_eph_priv.bin"),
440 load("m2.bin"),
441 load32("shared_secret.bin"),
442 ) else {
443 eprintln!("skipping x25519_matches_captured_shared_secret: fixtures absent");
444 return;
445 };
446 let map = Tlv8Map::parse(&m2).unwrap();
447 let accessory_eph: [u8; 32] = map.get(tlv::PUBLIC_KEY).unwrap().try_into().unwrap();
448 let kp = EphemeralKeypair::from_secret(eph_priv);
449 assert_eq!(kp.diffie_hellman(&accessory_eph), shared);
450 assert_eq!(
452 crate::x25519::x25519_shared(&eph_priv, &accessory_eph),
453 shared
454 );
455 }
456
457 #[test]
459 fn session_keys_match_captured() {
460 let (Some(shared), Some(read), Some(write)) = (
461 load32("shared_secret.bin"),
462 load32("control_read_encryption_key.bin"),
463 load32("control_write_encryption_key.bin"),
464 ) else {
465 eprintln!("skipping session_keys_match_captured: fixtures absent");
466 return;
467 };
468 assert_eq!(
469 derive_key(&shared, CONTROL_SALT, CONTROL_READ_INFO).unwrap(),
470 read
471 );
472 assert_eq!(
473 derive_key(&shared, CONTROL_SALT, CONTROL_WRITE_INFO).unwrap(),
474 write
475 );
476 }
477
478 #[test]
483 #[allow(clippy::unwrap_used)] fn coap_session_keys_match_aiohomekit() {
485 let shared: [u8; 32] = std::array::from_fn(|i| u8::try_from(i).unwrap_or(0)); let event = hex32("37c286d4ae336aeead7048a00b7762b642653d0e8aa691d4d3b7f0cf621db796");
489 let read = hex32("c09403ef8aa6c5045cbd8cf9bf3e665b2caed623af2be0e87c8f80f519914d3d");
490 let write = hex32("c3ca130c7033dbe5e7ff7f91d117ead869bac476994c7a48ca170c111136ed96");
491 assert_eq!(
492 derive_key(&shared, EVENT_SALT, EVENT_READ_INFO).unwrap(),
493 event
494 );
495 assert_eq!(
496 derive_key(&shared, CONTROL_SALT, CONTROL_READ_INFO).unwrap(),
497 read
498 );
499 assert_eq!(
500 derive_key(&shared, CONTROL_SALT, CONTROL_WRITE_INFO).unwrap(),
501 write
502 );
503 }
504
505 #[allow(clippy::unwrap_used)] fn hex32(s: &str) -> [u8; 32] {
507 let mut out = [0u8; 32];
508 for (i, b) in out.iter_mut().enumerate() {
509 *b = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).unwrap();
510 }
511 out
512 }
513
514 #[test]
518 fn full_replay_reaches_done_with_matching_keys() {
519 let (Some(accessory), Some(eph_priv), Some(m2), Some(m4), Some(read), Some(write)) = (
520 accessory_from_fixtures(),
521 load32("ios_eph_priv.bin"),
522 load("m2.bin"),
523 load("m4.bin"),
524 load32("control_read_encryption_key.bin"),
525 load32("control_write_encryption_key.bin"),
526 ) else {
527 eprintln!("skipping full_replay_reaches_done_with_matching_keys: fixtures absent");
528 return;
529 };
530
531 let mut client =
532 PairVerifyClient::new_with_ephemeral(&test_controller(), &accessory, eph_priv);
533 let _m1 = client.start();
534
535 let step = client.handle(&m2).unwrap();
537 let PairVerifyStep::Send(m3) = step else {
538 panic!("expected Send(m3) after M2, got {step:?}");
539 };
540 let m3map = Tlv8Map::parse(&m3).unwrap();
543 assert_eq!(m3map.get_u8(tlv::STATE).unwrap(), Some(tlv::STATE_M3));
544 assert!(m3map.get(tlv::ENCRYPTED_DATA).is_some());
545
546 let done = client.handle(&m4).unwrap();
548 let PairVerifyStep::Done(keys) = done else {
549 panic!("expected Done(SessionKeys) after M4, got {done:?}");
550 };
551 assert_eq!(keys.read_key, read);
552 assert_eq!(keys.write_key, write);
553 }
554
555 #[test]
558 fn corrupt_m2_encrypted_data_errors() {
559 let (Some(accessory), Some(eph_priv), Some(m2)) = (
560 accessory_from_fixtures(),
561 load32("ios_eph_priv.bin"),
562 load("m2.bin"),
563 ) else {
564 eprintln!("skipping corrupt_m2_encrypted_data_errors: fixtures absent");
565 return;
566 };
567
568 let map = Tlv8Map::parse(&m2).unwrap();
571 let accessory_eph = map.get(tlv::PUBLIC_KEY).unwrap().to_vec();
572 let mut enc = map.get(tlv::ENCRYPTED_DATA).unwrap().to_vec();
573 enc[0] ^= 0x01;
574
575 let mut tampered = Vec::new();
576 let mut w = Tlv8Writer::new(&mut tampered);
577 w.push_u8(tlv::STATE, tlv::STATE_M2);
578 w.push(tlv::PUBLIC_KEY, &accessory_eph);
579 w.push(tlv::ENCRYPTED_DATA, &enc);
580
581 let mut client =
582 PairVerifyClient::new_with_ephemeral(&test_controller(), &accessory, eph_priv);
583 let _m1 = client.start();
584 let err = client.handle(&tampered);
585 assert!(
586 matches!(err, Err(CryptoError::Aead | CryptoError::Signature)),
587 "expected Aead/Signature error, got {err:?}"
588 );
589 }
590
591 #[test]
593 fn handle_before_start_errors() {
594 let accessory = AccessoryPairing {
595 pairing_id: "AA:BB:CC:DD:EE:FF".to_string(),
596 ltpk: [0u8; 32],
597 };
598 let mut client = PairVerifyClient::new(&test_controller(), &accessory);
599 assert!(client.handle(b"\x06\x01\x02").is_err());
600 }
601
602 #[test]
604 fn accessory_error_in_m4_errors() {
605 let (Some(accessory), Some(eph_priv), Some(m2)) = (
606 accessory_from_fixtures(),
607 load32("ios_eph_priv.bin"),
608 load("m2.bin"),
609 ) else {
610 eprintln!("skipping accessory_error_in_m4_errors: fixtures absent");
611 return;
612 };
613 let mut client =
614 PairVerifyClient::new_with_ephemeral(&test_controller(), &accessory, eph_priv);
615 let _m1 = client.start();
616 client.handle(&m2).unwrap();
617 let mut m4err = Vec::new();
619 let mut w = Tlv8Writer::new(&mut m4err);
620 w.push_u8(tlv::STATE, tlv::STATE_M4);
621 w.push_u8(tlv::ERROR, 2);
622 assert!(client.handle(&m4err).is_err());
623 }
624
625 #[test]
628 fn broadcast_key_matches_direct_derive_after_done() {
629 let (Some(accessory), Some(eph_priv), Some(m2), Some(m4)) = (
630 accessory_from_fixtures(),
631 load32("ios_eph_priv.bin"),
632 load("m2.bin"),
633 load("m4.bin"),
634 ) else {
635 eprintln!("skipping broadcast_key_matches_direct_derive_after_done: fixtures absent");
636 return;
637 };
638
639 let controller = test_controller();
640 let mut client = PairVerifyClient::new_with_ephemeral(&controller, &accessory, eph_priv);
641 let _m1 = client.start();
642 client.handle(&m2).unwrap();
643 client.handle(&m4).unwrap();
644
645 let fake_ltpk = [0xABu8; 32];
648 let bk = client.broadcast_key(&fake_ltpk).unwrap();
649
650 let shared = {
653 let map = hap_tlv8::Tlv8Map::parse(&m2).unwrap();
654 let accessory_eph: [u8; 32] = map
655 .get(crate::tlv_types::PUBLIC_KEY)
656 .unwrap()
657 .try_into()
658 .unwrap();
659 crate::x25519::EphemeralKeypair::from_secret(eph_priv).diffie_hellman(&accessory_eph)
660 };
661 let direct = crate::BroadcastKey::derive(&shared, &fake_ltpk).unwrap();
662 assert_eq!(bk.as_bytes(), direct.as_bytes());
663 }
664
665 #[test]
667 fn broadcast_key_before_m2_errors() {
668 let accessory = AccessoryPairing {
669 pairing_id: "AA:BB:CC:DD:EE:FF".to_string(),
670 ltpk: [0u8; 32],
671 };
672 let mut client = PairVerifyClient::new(&test_controller(), &accessory);
673 let _m1 = client.start();
674 assert!(client.broadcast_key(&[0u8; 32]).is_err());
676 }
677}