Skip to main content

hap_crypto/
pair_setup.rs

1//! HomeKit Accessory Protocol **Pair Setup** controller state machine.
2//!
3//! Pair Setup (HAP specification chapter 5.6) is the six-message SRP-6a
4//! exchange by which a controller, knowing only the accessory's 8-digit setup
5//! code, establishes a mutually authenticated long-term pairing: it learns the
6//! accessory's Ed25519 long-term public key (LTPK) and the accessory learns the
7//! controller's. The messages are TLV8:
8//!
9//! | Msg | Direction      | Contents                                              |
10//! |-----|----------------|-------------------------------------------------------|
11//! | M1  | controller →   | `State=1`, `Method=PairSetup`                         |
12//! | M2  | → controller   | `State=2`, `Salt`, `PublicKey=B`                      |
13//! | M3  | controller →   | `State=3`, `PublicKey=A`, `Proof=M1`                  |
14//! | M4  | → controller   | `State=4`, `Proof=M2` (or `Error`)                    |
15//! | M5  | controller →   | `State=5`, `EncryptedData{ Id, LTPK, Signature }`     |
16//! | M6  | → controller   | `State=6`, `EncryptedData{ Id, LTPK, Signature }`     |
17//!
18//! The session encryption key for M5/M6 is
19//! `HKDF-SHA512(ikm = K, salt = "Pair-Setup-Encrypt-Salt",
20//! info = "Pair-Setup-Encrypt-Info", 32)`, where `K = H(S)` is the SRP session
21//! key derived from the premaster secret `S`. The controller signs
22//! `iOSDeviceX ‖ iOSPairingID ‖ iOS_LTPK` (with `iOSDeviceX` an HKDF of `K`
23//! under the controller-sign salt/info) and verifies the accessory's analogous
24//! signature in M6.
25//!
26//! The exact salt/info/nonce strings and concatenation order are cross-verified
27//! byte-for-byte against a captured `aiohomekit` Pair Setup trace (a real LIFX
28//! accessory) in this module's tests.
29//!
30//! # Usage
31//!
32//! Drive the machine by transport-agnostic message passing: send [`start`], then
33//! feed each accessory response to [`handle`] and send back whatever
34//! [`PairSetupStep::Send`] yields, until [`PairSetupStep::Done`] returns the
35//! established [`AccessoryPairing`].
36//!
37//! [`start`]: PairSetupClient::start
38//! [`handle`]: PairSetupClient::handle
39
40use hap_tlv8::{Tlv8Map, Tlv8Writer};
41use num_bigint::BigUint;
42use sha2::Sha512;
43
44use crate::aead::{decrypt, encrypt, hap_nonce};
45use crate::error::{CryptoError, Result};
46use crate::kdf::hkdf_sha512;
47use crate::keys::{verify_ed25519, ControllerKeypair};
48use crate::srp::{hap_group, SrpClient, SrpServer};
49use crate::tlv_types as tlv;
50
51/// The SRP-6a username HAP fixes for Pair Setup (`I` in RFC 5054 notation).
52const PAIR_SETUP_USERNAME: &[u8] = b"Pair-Setup";
53
54/// HKDF salt/info deriving the M5/M6 ChaCha20-Poly1305 session key from `K`.
55const ENCRYPT_SALT: &[u8] = b"Pair-Setup-Encrypt-Salt";
56const ENCRYPT_INFO: &[u8] = b"Pair-Setup-Encrypt-Info";
57/// HKDF salt/info deriving `iOSDeviceX`, the controller signing material.
58const CONTROLLER_SIGN_SALT: &[u8] = b"Pair-Setup-Controller-Sign-Salt";
59const CONTROLLER_SIGN_INFO: &[u8] = b"Pair-Setup-Controller-Sign-Info";
60/// HKDF salt/info deriving `AccessoryX`, the accessory signing material.
61const ACCESSORY_SIGN_SALT: &[u8] = b"Pair-Setup-Accessory-Sign-Salt";
62const ACCESSORY_SIGN_INFO: &[u8] = b"Pair-Setup-Accessory-Sign-Info";
63
64/// ChaCha20-Poly1305 nonce labels for the M5 and M6 encrypted sub-TLVs.
65const NONCE_M5: &[u8] = b"PS-Msg05";
66const NONCE_M6: &[u8] = b"PS-Msg06";
67
68/// The pairing material a successful Pair Setup yields about the accessory.
69///
70/// The controller stores this and uses it during every later Pair Verify to
71/// authenticate the accessory.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct AccessoryPairing {
74    /// The accessory's pairing identifier (`AccessoryPairingID`), a UTF-8 string
75    /// (typically a MAC-address-like value such as `AE:EC:86:C0:BF:D7`).
76    pub pairing_id: String,
77    /// The accessory's 32-byte Ed25519 long-term public key (`AccessoryLTPK`).
78    pub ltpk: [u8; 32],
79}
80
81/// The result of feeding one accessory response to [`PairSetupClient::handle`].
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum PairSetupStep {
84    /// The next controller message to transmit to the accessory (a TLV8 body).
85    Send(Vec<u8>),
86    /// Pair Setup completed; the accessory pairing was established and verified.
87    Done(AccessoryPairing),
88}
89
90/// Internal progress of the [`PairSetupClient`] exchange.
91enum State {
92    /// Before [`PairSetupClient::start`]; the M1 request has not been emitted.
93    Initial,
94    /// M1 sent; awaiting the accessory's M2 (salt + `B`).
95    AwaitingM2,
96    /// M3 sent; awaiting the accessory's M4 (`M2` proof). Holds the SRP session
97    /// key `K` and the controller proof `M1` needed to verify `M2`.
98    AwaitingM4 { session_key: Vec<u8>, m1: Vec<u8> },
99    /// M5 sent; awaiting the accessory's M6 (encrypted accessory sub-TLV). Holds
100    /// the SRP session key `K`.
101    AwaitingM6 { session_key: Vec<u8> },
102    /// The exchange finished (success or failure); no further input accepted.
103    Done,
104}
105
106/// A controller-side Pair Setup state machine over a single SRP-6a exchange.
107///
108/// Construct with [`new`](PairSetupClient::new), drive with
109/// [`start`](PairSetupClient::start) then [`handle`](PairSetupClient::handle).
110/// The machine is transport-agnostic: it consumes and produces raw TLV8 bodies.
111pub struct PairSetupClient {
112    /// The setup code as the SRP password `P` (e.g. `"123-45-678"`).
113    password: String,
114    controller: ControllerKeypair,
115    srp: SrpClient<Sha512>,
116    state: State,
117}
118
119impl PairSetupClient {
120    /// Create a Pair Setup client for `setup_code`, signing with `controller`.
121    ///
122    /// `setup_code` is the accessory's 8-digit setup code. It is accepted either
123    /// already hyphenated (`"123-45-678"`) or as bare digits (`"12345678"`); the
124    /// digits are re-grouped into the canonical `XXX-XX-XXX` form HAP hashes as
125    /// the SRP password. Any other input is used verbatim (the accessory will
126    /// then reject the proof, which surfaces as a setup-code error in M4).
127    ///
128    /// The SRP client ephemeral `a` is drawn from the OS CSPRNG; use
129    /// [`new_with_private`](PairSetupClient::new_with_private) for a
130    /// deterministic exchange in tests.
131    ///
132    /// # Errors
133    ///
134    /// Returns [`CryptoError::SrpBadParameters`] if the freshly generated SRP
135    /// public ephemeral `A` is zero mod `N` (vanishingly unlikely).
136    pub fn new(setup_code: &str, controller: ControllerKeypair) -> Result<Self> {
137        let srp = SrpClient::<Sha512>::new(hap_group()?, PAIR_SETUP_USERNAME)?;
138        Ok(Self {
139            password: normalize_setup_code(setup_code),
140            controller,
141            srp,
142            state: State::Initial,
143        })
144    }
145
146    /// Create a Pair Setup client with a caller-supplied SRP private ephemeral
147    /// `a` (the deterministic test seam).
148    ///
149    /// Mirrors the crate-internal `srp` module's `with_private` so a replay
150    /// harness can reproduce an exchange exactly. `a` is the big-endian bytes of
151    /// the SRP exponent.
152    ///
153    /// # Errors
154    ///
155    /// Returns [`CryptoError::SrpBadParameters`] if the resulting SRP public
156    /// ephemeral `A` is zero mod `N`.
157    pub fn new_with_private(
158        setup_code: &str,
159        controller: ControllerKeypair,
160        a: &[u8],
161    ) -> Result<Self> {
162        let srp = SrpClient::<Sha512>::with_private(
163            hap_group()?,
164            PAIR_SETUP_USERNAME,
165            BigUint::from_bytes_be(a),
166        )?;
167        Ok(Self {
168            password: normalize_setup_code(setup_code),
169            controller,
170            srp,
171            state: State::Initial,
172        })
173    }
174
175    /// Produce the M1 request that starts Pair Setup.
176    ///
177    /// The body is `State=1, Method=PairSetup`. Calling `start` advances the
178    /// machine to await M2; calling it again re-emits M1 but does not reset any
179    /// state already established by [`Self::handle`].
180    #[must_use]
181    pub fn start(&mut self) -> Vec<u8> {
182        self.state = State::AwaitingM2;
183        let mut out = Vec::new();
184        let mut w = Tlv8Writer::new(&mut out);
185        w.push_u8(tlv::STATE, tlv::STATE_M1);
186        w.push_u8(tlv::METHOD, tlv::METHOD_PAIR_SETUP);
187        out
188    }
189
190    /// Consume an accessory response and advance the exchange.
191    ///
192    /// Feed M2, then M4, then M6 in order; the return value is the next message
193    /// to send ([`PairSetupStep::Send`]) until the final
194    /// [`PairSetupStep::Done`] yields the [`AccessoryPairing`].
195    ///
196    /// # Errors
197    ///
198    /// Returns a [`CryptoError`] if the response is malformed, omits a required
199    /// field, carries an accessory `Error` TLV, fails SRP proof verification,
200    /// fails AEAD authentication, or carries an accessory signature that does
201    /// not verify. The machine then refuses further input.
202    pub fn handle(&mut self, response: &[u8]) -> Result<PairSetupStep> {
203        let map = Tlv8Map::parse(response)?;
204        check_error(&map)?;
205        match &self.state {
206            State::Initial => Err(CryptoError::Encoding("Pair Setup not started")),
207            State::AwaitingM2 => self.handle_m2(&map),
208            State::AwaitingM4 { .. } => self.handle_m4(&map),
209            State::AwaitingM6 { .. } => self.handle_m6(&map),
210            State::Done => Err(CryptoError::Encoding("Pair Setup already finished")),
211        }
212    }
213
214    /// M2 → produce M3. Consumes salt + `B`, derives `S`/`K`, builds `A`+`M1`.
215    fn handle_m2(&mut self, map: &Tlv8Map) -> Result<PairSetupStep> {
216        expect_state(map, tlv::STATE_M2)?;
217        let salt = map
218            .get(tlv::SALT)
219            .ok_or(CryptoError::Encoding("M2 missing salt"))?
220            .to_vec();
221        let b_bytes = map
222            .get(tlv::PUBLIC_KEY)
223            .ok_or(CryptoError::Encoding("M2 missing accessory public key B"))?;
224        let b_pub = BigUint::from_bytes_be(b_bytes);
225
226        let premaster = self
227            .srp
228            .premaster(&salt, self.password.as_bytes(), &b_pub)?;
229        let session_key = self.srp.session_key(&premaster);
230        let m1 = self.srp.proof_m1(&salt, &b_pub, &session_key);
231
232        let mut out = Vec::new();
233        let mut w = Tlv8Writer::new(&mut out);
234        w.push_u8(tlv::STATE, tlv::STATE_M3);
235        w.push(tlv::PUBLIC_KEY, &self.srp.a_pub_bytes());
236        w.push(tlv::PROOF, &m1);
237
238        self.state = State::AwaitingM4 { session_key, m1 };
239        Ok(PairSetupStep::Send(out))
240    }
241
242    /// M4 → produce M5. Verifies the accessory `M2` proof, then builds and seals
243    /// the controller sub-TLV `{ Identifier, PublicKey=LTPK, Signature }`.
244    fn handle_m4(&mut self, map: &Tlv8Map) -> Result<PairSetupStep> {
245        expect_state(map, tlv::STATE_M4)?;
246        let State::AwaitingM4 { session_key, m1 } = &self.state else {
247            return Err(CryptoError::Encoding("Pair Setup state corrupted"));
248        };
249        let session_key = session_key.clone();
250        let m1 = m1.clone();
251
252        let proof = map
253            .get(tlv::PROOF)
254            .ok_or(CryptoError::Encoding("M4 missing accessory proof M2"))?;
255        self.srp.verify_m2(&m1, &session_key, proof)?;
256
257        // Derive the M5/M6 encryption key and the controller signing material.
258        let mut enc_key = [0u8; 32];
259        hkdf_sha512(&session_key, ENCRYPT_SALT, ENCRYPT_INFO, &mut enc_key)?;
260        let mut ios_device_x = [0u8; 32];
261        hkdf_sha512(
262            &session_key,
263            CONTROLLER_SIGN_SALT,
264            CONTROLLER_SIGN_INFO,
265            &mut ios_device_x,
266        )?;
267
268        let id = self.controller.id.as_bytes();
269        let ltpk = self.controller.ltpk();
270
271        // sig = Ed25519(LTSK, iOSDeviceX ‖ iOSPairingID ‖ iOS_LTPK)
272        let mut signed = Vec::with_capacity(ios_device_x.len() + id.len() + ltpk.len());
273        signed.extend_from_slice(&ios_device_x);
274        signed.extend_from_slice(id);
275        signed.extend_from_slice(&ltpk);
276        let signature = self.controller.sign(&signed);
277
278        let mut sub = Vec::new();
279        let mut sw = Tlv8Writer::new(&mut sub);
280        sw.push(tlv::IDENTIFIER, id);
281        sw.push(tlv::PUBLIC_KEY, &ltpk);
282        sw.push(tlv::SIGNATURE, &signature);
283
284        let nonce = hap_nonce(NONCE_M5);
285        let sealed = encrypt(&enc_key, &nonce, b"", &sub)?;
286
287        let mut out = Vec::new();
288        let mut w = Tlv8Writer::new(&mut out);
289        w.push_u8(tlv::STATE, tlv::STATE_M5);
290        w.push(tlv::ENCRYPTED_DATA, &sealed);
291
292        self.state = State::AwaitingM6 { session_key };
293        Ok(PairSetupStep::Send(out))
294    }
295
296    /// M6 → finish. Decrypts the accessory sub-TLV, recomputes `AccessoryX`,
297    /// verifies the accessory signature, and yields the [`AccessoryPairing`].
298    fn handle_m6(&mut self, map: &Tlv8Map) -> Result<PairSetupStep> {
299        expect_state(map, tlv::STATE_M6)?;
300        let State::AwaitingM6 { session_key } = &self.state else {
301            return Err(CryptoError::Encoding("Pair Setup state corrupted"));
302        };
303        let session_key = session_key.clone();
304        self.state = State::Done;
305
306        let encrypted = map
307            .get(tlv::ENCRYPTED_DATA)
308            .ok_or(CryptoError::Encoding("M6 missing encrypted data"))?;
309
310        let mut enc_key = [0u8; 32];
311        hkdf_sha512(&session_key, ENCRYPT_SALT, ENCRYPT_INFO, &mut enc_key)?;
312        let nonce = hap_nonce(NONCE_M6);
313        let plaintext = decrypt(&enc_key, &nonce, b"", encrypted)?;
314
315        let sub = Tlv8Map::parse(&plaintext)?;
316        let id_bytes = sub
317            .get(tlv::IDENTIFIER)
318            .ok_or(CryptoError::Encoding("M6 sub-TLV missing identifier"))?;
319        let ltpk_bytes = sub
320            .get(tlv::PUBLIC_KEY)
321            .ok_or(CryptoError::Encoding("M6 sub-TLV missing public key"))?;
322        let signature = sub
323            .get(tlv::SIGNATURE)
324            .ok_or(CryptoError::Encoding("M6 sub-TLV missing signature"))?;
325
326        let ltpk: [u8; 32] = ltpk_bytes
327            .try_into()
328            .map_err(|_| CryptoError::Encoding("accessory LTPK is not 32 bytes"))?;
329        let signature: [u8; 64] = signature
330            .try_into()
331            .map_err(|_| CryptoError::Encoding("accessory signature is not 64 bytes"))?;
332        let pairing_id = String::from_utf8(id_bytes.to_vec())
333            .map_err(|_| CryptoError::Encoding("accessory pairing id is not valid UTF-8"))?;
334
335        // AccessoryX = HKDF(K, accessory-sign salt/info)
336        let mut accessory_x = [0u8; 32];
337        hkdf_sha512(
338            &session_key,
339            ACCESSORY_SIGN_SALT,
340            ACCESSORY_SIGN_INFO,
341            &mut accessory_x,
342        )?;
343
344        // Verify Ed25519(LTPK, AccessoryX ‖ AccessoryPairingID ‖ AccessoryLTPK).
345        let mut signed = Vec::with_capacity(accessory_x.len() + id_bytes.len() + ltpk.len());
346        signed.extend_from_slice(&accessory_x);
347        signed.extend_from_slice(id_bytes);
348        signed.extend_from_slice(&ltpk);
349        verify_ed25519(&ltpk, &signed, &signature)?;
350
351        Ok(PairSetupStep::Done(AccessoryPairing { pairing_id, ltpk }))
352    }
353}
354
355/// The accessory (server) half of HAP Pair Setup's SRP-6a exchange.
356///
357/// This is the counterpart of [`PairSetupClient`]'s SRP portion (M2–M4),
358/// packaged for a reference accessory to drive Pair Setup without touching the
359/// crate-internal SRP generics: it fixes the HAP group (RFC 5054 Appendix A
360/// 3072-bit, `g = 5`), the hash (SHA-512), and the SRP username (`"Pair-Setup"`),
361/// and normalises the setup code exactly as [`PairSetupClient`] does so the two
362/// derive the same verifier.
363///
364/// Construct one per pairing attempt with [`new`](HapPairSetupSrpServer::new)
365/// (a fresh random salt and private ephemeral `b`), send M2 as
366/// `State=2, Salt=`[`salt`](HapPairSetupSrpServer::new)`, PublicKey=`
367/// [`b_pub_bytes`](HapPairSetupSrpServer::b_pub_bytes), then on M3 compute the
368/// session key from the controller's `A` with
369/// [`session_key`](HapPairSetupSrpServer::session_key) and verify its proof with
370/// [`verify_m1_prove_m2`](HapPairSetupSrpServer::verify_m1_prove_m2).
371pub struct HapPairSetupSrpServer {
372    inner: SrpServer<Sha512>,
373}
374
375impl HapPairSetupSrpServer {
376    /// Build a Pair Setup SRP server for `setup_code`, returning it together with
377    /// the 16-byte SRP salt to send in M2.
378    ///
379    /// `setup_code` is accepted hyphenated (`"123-45-678"`) or as bare digits
380    /// (`"12345678"`); it is normalised to the canonical `XXX-XX-XXX` form —
381    /// identically to [`PairSetupClient::new`] — before use as the SRP password.
382    /// The salt and the private ephemeral `b` are drawn from the OS CSPRNG.
383    ///
384    /// # Errors
385    ///
386    /// Returns [`CryptoError`] only if the embedded HAP group is rejected (which
387    /// the fixed constant never triggers).
388    pub fn new(setup_code: &str) -> Result<(Self, Vec<u8>)> {
389        let password = normalize_setup_code(setup_code);
390        let inner =
391            SrpServer::<Sha512>::new(hap_group()?, PAIR_SETUP_USERNAME, password.as_bytes());
392        let salt = inner.salt().to_vec();
393        Ok((Self { inner }, salt))
394    }
395
396    /// `PAD(B)` — the accessory's SRP public ephemeral in wire form (384 bytes),
397    /// sent as `PublicKey` in M2.
398    #[must_use]
399    pub fn b_pub_bytes(&self) -> Vec<u8> {
400        self.inner.b_pub_bytes()
401    }
402
403    /// Compute the SRP session key `K` from the controller's public ephemeral
404    /// `A` (the `PublicKey` bytes received in M3).
405    ///
406    /// # Errors
407    ///
408    /// Returns [`CryptoError::SrpBadParameters`] if `A` is zero mod `N`, or
409    /// [`CryptoError::SrpProofMismatch`] if the scrambler `u` is zero.
410    pub fn session_key(&self, a_pub_bytes: &[u8]) -> Result<Vec<u8>> {
411        let a_pub = BigUint::from_bytes_be(a_pub_bytes);
412        self.inner.session_key(&a_pub)
413    }
414
415    /// Verify the controller's proof `M1` (from M3) against its public ephemeral
416    /// `A`, returning the accessory proof `M2` to send in M4.
417    ///
418    /// The session key is recomputed from `A` internally, so this is
419    /// self-contained; callers that also need `K` (to derive the M5/M6 keys)
420    /// should call [`session_key`](HapPairSetupSrpServer::session_key) once and
421    /// keep the result.
422    ///
423    /// # Errors
424    ///
425    /// Returns [`CryptoError::SrpProofMismatch`] if `M1` does not verify (the
426    /// wrong setup code), or the errors of
427    /// [`session_key`](HapPairSetupSrpServer::session_key).
428    pub fn verify_m1_prove_m2(&self, a_pub_bytes: &[u8], m1: &[u8]) -> Result<Vec<u8>> {
429        let a_pub = BigUint::from_bytes_be(a_pub_bytes);
430        let session_key = self.inner.session_key(&a_pub)?;
431        self.inner.verify_m1_prove_m2(&a_pub, &session_key, m1)
432    }
433}
434
435/// Normalise a setup code to the canonical `XXX-XX-XXX` SRP password form.
436///
437/// Exactly eight digits (with any non-digit characters stripped) are re-grouped;
438/// anything else is returned unchanged so an already-formatted or unexpected
439/// code still reaches SRP verbatim.
440fn normalize_setup_code(code: &str) -> String {
441    let digits: String = code.chars().filter(char::is_ascii_digit).collect();
442    if digits.len() == 8 {
443        format!("{}-{}-{}", &digits[0..3], &digits[3..5], &digits[5..8])
444    } else {
445        code.to_string()
446    }
447}
448
449/// Map an accessory `Error` TLV to a [`CryptoError`], if present.
450fn check_error(map: &Tlv8Map) -> Result<()> {
451    match map.get(tlv::ERROR) {
452        None | Some([]) => Ok(()),
453        // Any non-empty error code aborts the exchange. SRP-credential failures
454        // (kTLVError_Authentication = 2) are the common case (wrong setup code).
455        Some([2]) => Err(CryptoError::SrpProofMismatch),
456        Some(_) => Err(CryptoError::Encoding("accessory returned a pairing error")),
457    }
458}
459
460/// Require the response to carry the expected `State` value.
461fn expect_state(map: &Tlv8Map, expected: u8) -> Result<()> {
462    match map.get_u8(tlv::STATE)? {
463        // Some accessories omit State (a known quirk); tolerate that.
464        None => Ok(()),
465        Some(s) if s == expected => Ok(()),
466        Some(_) => Err(CryptoError::Encoding("unexpected Pair Setup state")),
467    }
468}
469
470#[cfg(test)]
471// Test code only: CLAUDE.md carves out `unwrap`/`expect` for tests with a
472// documented justification. Fixtures are fixed captured/known values, so a
473// failing `unwrap` here is itself a test failure, which is intended.
474#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
475mod tests {
476    use super::*;
477    use crate::srp::{compute_b, compute_k, compute_u, compute_v, compute_x, SrpGroup};
478    use ed25519_dalek::Signer;
479    use sha2::{Digest, Sha512};
480
481    /// Load a committed fixture from the workspace `test-vectors/` tree.
482    fn fixture(rel: &str) -> Option<Vec<u8>> {
483        let p = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
484            .join("../../test-vectors")
485            .join(rel);
486        std::fs::read(p).ok()
487    }
488
489    fn test_controller() -> ControllerKeypair {
490        // RFC 8032 TEST 2 seed — any fixed seed gives a deterministic LTPK.
491        let seed = [
492            0x4c, 0xcd, 0x08, 0x9b, 0x28, 0xff, 0x96, 0xda, 0x9d, 0xb6, 0xc3, 0x46, 0xec, 0x11,
493            0x4e, 0x0f, 0x5b, 0x8a, 0x31, 0x9f, 0x35, 0xab, 0xa6, 0x24, 0xda, 0x8c, 0xf6, 0xed,
494            0x4f, 0xb8, 0xa6, 0xfb,
495        ];
496        ControllerKeypair::from_seed("test-controller".to_string(), seed)
497    }
498
499    // ---- M1 reproduction against the real captured trace ----
500
501    #[test]
502    fn m1_reproduces_captured_trace_byte_for_byte() {
503        let Some(expected) = fixture("pair-setup/m1.bin") else {
504            eprintln!("skipping: no test-vectors/pair-setup/m1.bin");
505            return;
506        };
507        let mut client = PairSetupClient::new("123-45-678", test_controller()).unwrap();
508        let m1 = client.start();
509        assert_eq!(
510            m1, expected,
511            "M1 must match the captured trace byte-for-byte"
512        );
513    }
514
515    // ---- M6 decrypt + accessory-signature verify against the real trace ----
516    //
517    // Uses the captured premaster S only (no ephemeral secret needed): we derive
518    // K = H(S), drive the M6 path of the machine directly, and assert the
519    // accessory signature verifies and we recover the AccessoryPairing.
520
521    /// The SRP session key `K = H(PAD(S))` from the captured premaster secret.
522    fn captured_session_key() -> Option<Vec<u8>> {
523        let s = fixture("srp/S.bin")?;
524        // S.bin is already PAD'd to len(N) = 384 bytes; K = SHA-512(S).
525        Some(Sha512::digest(&s).to_vec())
526    }
527
528    #[test]
529    fn m6_decrypts_and_verifies_accessory_signature_from_real_trace() {
530        let (Some(m6), Some(session_key)) = (fixture("pair-setup/m6.bin"), captured_session_key())
531        else {
532            eprintln!("skipping: no captured S.bin / m6.bin");
533            return;
534        };
535
536        let mut client = PairSetupClient::new("000-00-000", test_controller()).unwrap();
537        // Inject the captured session key and put the machine in the M6 state.
538        client.state = State::AwaitingM6 { session_key };
539
540        let step = client.handle(&m6).expect("M6 must decrypt and verify");
541        let PairSetupStep::Done(pairing) = step else {
542            panic!("expected Done, got {step:?}");
543        };
544        assert_eq!(pairing.pairing_id, "AE:EC:86:C0:BF:D7");
545        assert_eq!(pairing.ltpk.len(), 32);
546        assert!(!pairing.pairing_id.is_empty());
547    }
548
549    // ---- M5 decrypt against the real trace ----
550    //
551    // Decrypt the captured M5 request and verify the controller signature it
552    // carries under the embedded PublicKey, over iOSDeviceX ‖ id ‖ pubkey.
553
554    #[test]
555    fn m5_decrypts_and_controller_signature_verifies_from_real_trace() {
556        let (Some(m5), Some(session_key)) = (fixture("pair-setup/m5.bin"), captured_session_key())
557        else {
558            eprintln!("skipping: no captured S.bin / m5.bin");
559            return;
560        };
561
562        let map = Tlv8Map::parse(&m5).unwrap();
563        let encrypted = map.get(tlv::ENCRYPTED_DATA).unwrap();
564
565        let mut enc_key = [0u8; 32];
566        hkdf_sha512(&session_key, ENCRYPT_SALT, ENCRYPT_INFO, &mut enc_key).unwrap();
567        let nonce = hap_nonce(NONCE_M5);
568        let plaintext = decrypt(&enc_key, &nonce, b"", encrypted).unwrap();
569
570        let sub = Tlv8Map::parse(&plaintext).unwrap();
571        let id = sub.get(tlv::IDENTIFIER).unwrap();
572        let pubkey = sub.get(tlv::PUBLIC_KEY).unwrap();
573        let sig = sub.get(tlv::SIGNATURE).unwrap();
574        assert_eq!(pubkey.len(), 32, "controller LTPK is 32 bytes");
575        assert_eq!(sig.len(), 64, "controller signature is 64 bytes");
576
577        let mut ios_device_x = [0u8; 32];
578        hkdf_sha512(
579            &session_key,
580            CONTROLLER_SIGN_SALT,
581            CONTROLLER_SIGN_INFO,
582            &mut ios_device_x,
583        )
584        .unwrap();
585
586        let mut signed = Vec::new();
587        signed.extend_from_slice(&ios_device_x);
588        signed.extend_from_slice(id);
589        signed.extend_from_slice(pubkey);
590
591        let ltpk: [u8; 32] = pubkey.try_into().unwrap();
592        let signature: [u8; 64] = sig.try_into().unwrap();
593        verify_ed25519(&ltpk, &signed, &signature)
594            .expect("captured M5 controller signature must verify");
595    }
596
597    // ---- Self-consistency replay: full machine vs a test "accessory" ----
598
599    /// A minimal test accessory: holds the verifier, a fixed `b`, and an Ed25519
600    /// long-term keypair, and produces M2/M4/M6 the way a real accessory would.
601    struct TestAccessory {
602        group: SrpGroup,
603        pairing_id: String,
604        signing: ed25519_dalek::SigningKey,
605        salt: Vec<u8>,
606        b_priv: BigUint,
607        b_pub: BigUint,
608        session_key: Option<Vec<u8>>,
609        a_pub: Option<BigUint>,
610    }
611
612    impl TestAccessory {
613        fn new(password: &str) -> Self {
614            let group = hap_group().unwrap();
615            let salt = vec![0x11u8; 16];
616            let x = compute_x::<Sha512>(&salt, PAIR_SETUP_USERNAME, password.as_bytes());
617            let v = compute_v(&group, &x);
618            let k = compute_k::<Sha512>(&group);
619            let b_priv = BigUint::from_bytes_be(&[0x5Au8; 32]);
620            let b_pub = compute_b(&group, &k, &v, &b_priv);
621            let signing = ed25519_dalek::SigningKey::from_bytes(&[0x99u8; 32]);
622            Self {
623                group,
624                pairing_id: "11:22:33:44:55:66".to_string(),
625                signing,
626                salt,
627                b_priv,
628                b_pub,
629                session_key: None,
630                a_pub: None,
631            }
632        }
633
634        /// Respond to M3 with M2 framing (salt + B). (Sent before the controller
635        /// sends M3, but built from no controller input.)
636        fn m2(&self) -> Vec<u8> {
637            let mut out = Vec::new();
638            let mut w = Tlv8Writer::new(&mut out);
639            w.push_u8(tlv::STATE, tlv::STATE_M2);
640            w.push(tlv::SALT, &self.salt);
641            w.push(tlv::PUBLIC_KEY, &pad_be(&self.b_pub, 384));
642            out
643        }
644
645        /// Consume the controller's M3 (A + M1 proof), compute the shared secret
646        /// and session key, verify M1, and produce M4 (M2 proof).
647        fn m4(&mut self, m3: &[u8]) -> Vec<u8> {
648            let map = Tlv8Map::parse(m3).unwrap();
649            let a_bytes = map.get(tlv::PUBLIC_KEY).unwrap();
650            let a_pub = BigUint::from_bytes_be(a_bytes);
651            let m1 = map.get(tlv::PROOF).unwrap().to_vec();
652
653            let modulus = self.group.modulus();
654            // Verifier-side premaster: S = (A * v^u) ^ b mod N, with the
655            // verifier v recomputed from the (test-known) salt and password.
656            let scrambler = compute_u::<Sha512>(&self.group, &a_pub, &self.b_pub);
657            let x_priv =
658                compute_x::<Sha512>(&self.salt, PAIR_SETUP_USERNAME, TEST_PASSWORD.as_bytes());
659            let verifier = compute_v(&self.group, &x_priv);
660            let vu = verifier.modpow(&scrambler, modulus);
661            let base = (&a_pub * &vu) % modulus;
662            let premaster = base.modpow(&self.b_priv, modulus);
663            let session_key = Sha512::digest(pad_be(&premaster, 384)).to_vec();
664
665            // Verify the controller's M1 proof:
666            // M1 = H(H(N) xor H(g) | H(I) | s | A | B | K)
667            let expected_m1 = self.controller_m1(&a_pub, &session_key);
668            assert_eq!(m1, expected_m1, "test accessory: controller M1 must verify");
669
670            self.a_pub = Some(a_pub);
671            self.session_key = Some(session_key.clone());
672
673            let m2_proof = {
674                let mut h = Sha512::new();
675                h.update(pad_be(self.a_pub.as_ref().unwrap(), 384));
676                h.update(&m1);
677                h.update(&session_key);
678                h.finalize().to_vec()
679            };
680
681            let mut out = Vec::new();
682            let mut w = Tlv8Writer::new(&mut out);
683            w.push_u8(tlv::STATE, tlv::STATE_M4);
684            w.push(tlv::PROOF, &m2_proof);
685            out
686        }
687
688        fn controller_m1(&self, a_pub: &BigUint, session_key: &[u8]) -> Vec<u8> {
689            let h_n = Sha512::digest(self.group.modulus().to_bytes_be());
690            let h_g = Sha512::digest(self.group.generator().to_bytes_be());
691            let h_xor: Vec<u8> = h_n.iter().zip(h_g.iter()).map(|(a, b)| a ^ b).collect();
692            let h_i = Sha512::digest(PAIR_SETUP_USERNAME);
693            let mut h = Sha512::new();
694            h.update(h_xor);
695            h.update(h_i);
696            h.update(&self.salt);
697            h.update(pad_be(a_pub, 384));
698            h.update(pad_be(&self.b_pub, 384));
699            h.update(session_key);
700            h.finalize().to_vec()
701        }
702
703        /// Consume the controller's M5 (encrypted controller sub-TLV), then
704        /// produce M6 (encrypted accessory sub-TLV with a valid signature).
705        fn m6(&self, _m5: &[u8]) -> Vec<u8> {
706            let session_key = self.session_key.as_ref().unwrap();
707            let mut accessory_x = [0u8; 32];
708            hkdf_sha512(
709                session_key,
710                ACCESSORY_SIGN_SALT,
711                ACCESSORY_SIGN_INFO,
712                &mut accessory_x,
713            )
714            .unwrap();
715            let ltpk = self.signing.verifying_key().to_bytes();
716            let id = self.pairing_id.as_bytes();
717
718            let mut signed = Vec::new();
719            signed.extend_from_slice(&accessory_x);
720            signed.extend_from_slice(id);
721            signed.extend_from_slice(&ltpk);
722            let sig = self.signing.sign(&signed).to_bytes();
723
724            let mut sub = Vec::new();
725            let mut sw = Tlv8Writer::new(&mut sub);
726            sw.push(tlv::IDENTIFIER, id);
727            sw.push(tlv::PUBLIC_KEY, &ltpk);
728            sw.push(tlv::SIGNATURE, &sig);
729
730            let mut enc_key = [0u8; 32];
731            hkdf_sha512(session_key, ENCRYPT_SALT, ENCRYPT_INFO, &mut enc_key).unwrap();
732            let sealed = encrypt(&enc_key, &hap_nonce(NONCE_M6), b"", &sub).unwrap();
733
734            let mut out = Vec::new();
735            let mut w = Tlv8Writer::new(&mut out);
736            w.push_u8(tlv::STATE, tlv::STATE_M6);
737            w.push(tlv::ENCRYPTED_DATA, &sealed);
738            out
739        }
740    }
741
742    const TEST_PASSWORD: &str = "123-45-678";
743
744    /// Big-endian bytes left-padded to `width` (mirrors SRP `PAD`).
745    fn pad_be(v: &BigUint, width: usize) -> Vec<u8> {
746        let raw = v.to_bytes_be();
747        if raw.len() >= width {
748            return raw;
749        }
750        let mut out = vec![0u8; width - raw.len()];
751        out.extend_from_slice(&raw);
752        out
753    }
754
755    #[test]
756    fn full_machine_replay_reaches_done() {
757        let mut accessory = TestAccessory::new(TEST_PASSWORD);
758        let a = [0x37u8; 32];
759        let mut client =
760            PairSetupClient::new_with_private(TEST_PASSWORD, test_controller(), &a).unwrap();
761
762        let m1 = client.start();
763        assert_eq!(
764            Tlv8Map::parse(&m1).unwrap().get_u8(tlv::STATE).unwrap(),
765            Some(tlv::STATE_M1)
766        );
767
768        // Accessory replies with M2.
769        let m2 = accessory.m2();
770        let PairSetupStep::Send(m3) = client.handle(&m2).unwrap() else {
771            panic!("expected M3");
772        };
773
774        // Accessory verifies M3, replies with M4.
775        let m4 = accessory.m4(&m3);
776        let PairSetupStep::Send(m5) = client.handle(&m4).unwrap() else {
777            panic!("expected M5");
778        };
779
780        // Accessory replies with M6.
781        let m6 = accessory.m6(&m5);
782        let PairSetupStep::Done(pairing) = client.handle(&m6).unwrap() else {
783            panic!("expected Done");
784        };
785        assert_eq!(pairing.pairing_id, "11:22:33:44:55:66");
786        assert_eq!(pairing.ltpk, accessory.signing.verifying_key().to_bytes());
787    }
788
789    #[test]
790    fn wrong_setup_code_fails_m4_proof() {
791        let accessory = TestAccessory::new(TEST_PASSWORD);
792        let a = [0x37u8; 32];
793        // Controller uses a different code: M1 proof will not match → M2 proof
794        // computed by the accessory differs → verify_m2 rejects it in M4.
795        let mut client =
796            PairSetupClient::new_with_private("999-99-999", test_controller(), &a).unwrap();
797        let _ = client.start();
798        let m2 = accessory.m2();
799        let PairSetupStep::Send(m3) = client.handle(&m2).unwrap() else {
800            panic!("expected M3");
801        };
802        // The test accessory asserts the controller M1 internally; with a wrong
803        // code that assert would fire, so instead drive M4 verification directly:
804        // build an M4 with a deliberately wrong proof.
805        let _ = m3;
806        let mut bad_m4 = Vec::new();
807        let mut w = Tlv8Writer::new(&mut bad_m4);
808        w.push_u8(tlv::STATE, tlv::STATE_M4);
809        w.push(tlv::PROOF, &[0u8; 64]);
810        assert!(matches!(
811            client.handle(&bad_m4),
812            Err(CryptoError::SrpProofMismatch)
813        ));
814    }
815
816    #[test]
817    fn accessory_error_tlv_is_surfaced() {
818        let a = [0x37u8; 32];
819        let mut client =
820            PairSetupClient::new_with_private(TEST_PASSWORD, test_controller(), &a).unwrap();
821        let _ = client.start();
822        // M2 carrying an Authentication error instead of salt/B.
823        let mut err = Vec::new();
824        let mut w = Tlv8Writer::new(&mut err);
825        w.push_u8(tlv::STATE, tlv::STATE_M2);
826        w.push_u8(tlv::ERROR, 2); // kTLVError_Authentication
827        assert!(matches!(
828            client.handle(&err),
829            Err(CryptoError::SrpProofMismatch)
830        ));
831    }
832
833    #[test]
834    fn handle_before_start_errors() {
835        let a = [0x37u8; 32];
836        let mut client =
837            PairSetupClient::new_with_private(TEST_PASSWORD, test_controller(), &a).unwrap();
838        assert!(client.handle(b"").is_err());
839    }
840
841    #[test]
842    fn normalize_setup_code_regroups_bare_digits() {
843        assert_eq!(normalize_setup_code("12345678"), "123-45-678");
844        assert_eq!(normalize_setup_code("123-45-678"), "123-45-678");
845        assert_eq!(normalize_setup_code("oddball"), "oddball");
846    }
847
848    // ---- The public accessory-side SRP wrapper ----
849    //
850    // `HapPairSetupSrpServer` is the accessory counterpart of `PairSetupClient`'s
851    // SRP half. Drive a full M1..M4 SRP handshake between the real client and the
852    // wrapper: if the client accepts M4 (advances to M5) then the salt, `B`, the
853    // session key `K`, and both proofs all agreed byte-for-byte.
854
855    #[test]
856    fn hap_pair_setup_srp_server_interoperates_with_client() {
857        let (server, salt) = HapPairSetupSrpServer::new("123-45-678").unwrap();
858
859        let a = [0x37u8; 32];
860        let mut client =
861            PairSetupClient::new_with_private("123-45-678", test_controller(), &a).unwrap();
862        let _m1 = client.start();
863
864        // Accessory M2: State=2, Salt, PublicKey=B.
865        let mut m2 = Vec::new();
866        let mut w = Tlv8Writer::new(&mut m2);
867        w.push_u8(tlv::STATE, tlv::STATE_M2);
868        w.push(tlv::SALT, &salt);
869        w.push(tlv::PUBLIC_KEY, &server.b_pub_bytes());
870
871        // Client consumes M2, emits M3 (A + proof M1).
872        let PairSetupStep::Send(m3) = client.handle(&m2).unwrap() else {
873            panic!("expected M3 from the client");
874        };
875        let m3_map = Tlv8Map::parse(&m3).unwrap();
876        let a_pub = m3_map.get(tlv::PUBLIC_KEY).unwrap();
877        let m1 = m3_map.get(tlv::PROOF).unwrap();
878
879        // The server's session key must equal the client's (proven transitively
880        // via the proofs), and it must verify M1 and return a matching M2.
881        let m2_proof = server.verify_m1_prove_m2(a_pub, m1).unwrap();
882
883        // Feed M4 (State=4, Proof=M2) to the client: accepting it (advancing to
884        // M5) means the whole SRP half agreed.
885        let mut m4 = Vec::new();
886        let mut w = Tlv8Writer::new(&mut m4);
887        w.push_u8(tlv::STATE, tlv::STATE_M4);
888        w.push(tlv::PROOF, &m2_proof);
889        assert!(
890            matches!(client.handle(&m4).unwrap(), PairSetupStep::Send(_)),
891            "client must accept M4 and emit M5"
892        );
893    }
894
895    #[test]
896    fn hap_pair_setup_srp_server_rejects_wrong_setup_code() {
897        // The controller proves knowledge of "123-45-678"; a server built for a
898        // different code computes a different verifier, so M1 must not verify.
899        let (setup_server, salt) = HapPairSetupSrpServer::new("123-45-678").unwrap();
900        let a = [0x37u8; 32];
901        let mut client =
902            PairSetupClient::new_with_private("123-45-678", test_controller(), &a).unwrap();
903        let _m1 = client.start();
904
905        let mut m2 = Vec::new();
906        let mut w = Tlv8Writer::new(&mut m2);
907        w.push_u8(tlv::STATE, tlv::STATE_M2);
908        w.push(tlv::SALT, &salt);
909        w.push(tlv::PUBLIC_KEY, &setup_server.b_pub_bytes());
910        let PairSetupStep::Send(m3) = client.handle(&m2).unwrap() else {
911            panic!("expected M3");
912        };
913        let m3_map = Tlv8Map::parse(&m3).unwrap();
914        let a_pub = m3_map.get(tlv::PUBLIC_KEY).unwrap();
915        let m1 = m3_map.get(tlv::PROOF).unwrap();
916
917        // A different server (wrong code) must reject that M1.
918        let (wrong_server, _) = HapPairSetupSrpServer::new("999-99-999").unwrap();
919        assert!(matches!(
920            wrong_server.verify_m1_prove_m2(a_pub, m1),
921            Err(CryptoError::SrpProofMismatch)
922        ));
923    }
924}