1use 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};
49use crate::tlv_types as tlv;
50
51const PAIR_SETUP_USERNAME: &[u8] = b"Pair-Setup";
53
54const ENCRYPT_SALT: &[u8] = b"Pair-Setup-Encrypt-Salt";
56const ENCRYPT_INFO: &[u8] = b"Pair-Setup-Encrypt-Info";
57const CONTROLLER_SIGN_SALT: &[u8] = b"Pair-Setup-Controller-Sign-Salt";
59const CONTROLLER_SIGN_INFO: &[u8] = b"Pair-Setup-Controller-Sign-Info";
60const ACCESSORY_SIGN_SALT: &[u8] = b"Pair-Setup-Accessory-Sign-Salt";
62const ACCESSORY_SIGN_INFO: &[u8] = b"Pair-Setup-Accessory-Sign-Info";
63
64const NONCE_M5: &[u8] = b"PS-Msg05";
66const NONCE_M6: &[u8] = b"PS-Msg06";
67
68#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct AccessoryPairing {
74 pub pairing_id: String,
77 pub ltpk: [u8; 32],
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum PairSetupStep {
84 Send(Vec<u8>),
86 Done(AccessoryPairing),
88}
89
90enum State {
92 Initial,
94 AwaitingM2,
96 AwaitingM4 { session_key: Vec<u8>, m1: Vec<u8> },
99 AwaitingM6 { session_key: Vec<u8> },
102 Done,
104}
105
106pub struct PairSetupClient {
112 password: String,
114 controller: ControllerKeypair,
115 srp: SrpClient<Sha512>,
116 state: State,
117}
118
119impl PairSetupClient {
120 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 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 #[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 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 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 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 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 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(<pk);
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, <pk);
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 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 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 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(<pk);
349 verify_ed25519(<pk, &signed, &signature)?;
350
351 Ok(PairSetupStep::Done(AccessoryPairing { pairing_id, ltpk }))
352 }
353}
354
355fn normalize_setup_code(code: &str) -> String {
361 let digits: String = code.chars().filter(char::is_ascii_digit).collect();
362 if digits.len() == 8 {
363 format!("{}-{}-{}", &digits[0..3], &digits[3..5], &digits[5..8])
364 } else {
365 code.to_string()
366 }
367}
368
369fn check_error(map: &Tlv8Map) -> Result<()> {
371 match map.get(tlv::ERROR) {
372 None | Some([]) => Ok(()),
373 Some([2]) => Err(CryptoError::SrpProofMismatch),
376 Some(_) => Err(CryptoError::Encoding("accessory returned a pairing error")),
377 }
378}
379
380fn expect_state(map: &Tlv8Map, expected: u8) -> Result<()> {
382 match map.get_u8(tlv::STATE)? {
383 None => Ok(()),
385 Some(s) if s == expected => Ok(()),
386 Some(_) => Err(CryptoError::Encoding("unexpected Pair Setup state")),
387 }
388}
389
390#[cfg(test)]
391#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
395mod tests {
396 use super::*;
397 use crate::srp::{compute_b, compute_k, compute_u, compute_v, compute_x, SrpGroup};
398 use ed25519_dalek::Signer;
399 use sha2::{Digest, Sha512};
400
401 fn fixture(rel: &str) -> Option<Vec<u8>> {
403 let p = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
404 .join("../../test-vectors")
405 .join(rel);
406 std::fs::read(p).ok()
407 }
408
409 fn test_controller() -> ControllerKeypair {
410 let seed = [
412 0x4c, 0xcd, 0x08, 0x9b, 0x28, 0xff, 0x96, 0xda, 0x9d, 0xb6, 0xc3, 0x46, 0xec, 0x11,
413 0x4e, 0x0f, 0x5b, 0x8a, 0x31, 0x9f, 0x35, 0xab, 0xa6, 0x24, 0xda, 0x8c, 0xf6, 0xed,
414 0x4f, 0xb8, 0xa6, 0xfb,
415 ];
416 ControllerKeypair::from_seed("test-controller".to_string(), seed)
417 }
418
419 #[test]
422 fn m1_reproduces_captured_trace_byte_for_byte() {
423 let Some(expected) = fixture("pair-setup/m1.bin") else {
424 eprintln!("skipping: no test-vectors/pair-setup/m1.bin");
425 return;
426 };
427 let mut client = PairSetupClient::new("123-45-678", test_controller()).unwrap();
428 let m1 = client.start();
429 assert_eq!(
430 m1, expected,
431 "M1 must match the captured trace byte-for-byte"
432 );
433 }
434
435 fn captured_session_key() -> Option<Vec<u8>> {
443 let s = fixture("srp/S.bin")?;
444 Some(Sha512::digest(&s).to_vec())
446 }
447
448 #[test]
449 fn m6_decrypts_and_verifies_accessory_signature_from_real_trace() {
450 let (Some(m6), Some(session_key)) = (fixture("pair-setup/m6.bin"), captured_session_key())
451 else {
452 eprintln!("skipping: no captured S.bin / m6.bin");
453 return;
454 };
455
456 let mut client = PairSetupClient::new("000-00-000", test_controller()).unwrap();
457 client.state = State::AwaitingM6 { session_key };
459
460 let step = client.handle(&m6).expect("M6 must decrypt and verify");
461 let PairSetupStep::Done(pairing) = step else {
462 panic!("expected Done, got {step:?}");
463 };
464 assert_eq!(pairing.pairing_id, "AE:EC:86:C0:BF:D7");
465 assert_eq!(pairing.ltpk.len(), 32);
466 assert!(!pairing.pairing_id.is_empty());
467 }
468
469 #[test]
475 fn m5_decrypts_and_controller_signature_verifies_from_real_trace() {
476 let (Some(m5), Some(session_key)) = (fixture("pair-setup/m5.bin"), captured_session_key())
477 else {
478 eprintln!("skipping: no captured S.bin / m5.bin");
479 return;
480 };
481
482 let map = Tlv8Map::parse(&m5).unwrap();
483 let encrypted = map.get(tlv::ENCRYPTED_DATA).unwrap();
484
485 let mut enc_key = [0u8; 32];
486 hkdf_sha512(&session_key, ENCRYPT_SALT, ENCRYPT_INFO, &mut enc_key).unwrap();
487 let nonce = hap_nonce(NONCE_M5);
488 let plaintext = decrypt(&enc_key, &nonce, b"", encrypted).unwrap();
489
490 let sub = Tlv8Map::parse(&plaintext).unwrap();
491 let id = sub.get(tlv::IDENTIFIER).unwrap();
492 let pubkey = sub.get(tlv::PUBLIC_KEY).unwrap();
493 let sig = sub.get(tlv::SIGNATURE).unwrap();
494 assert_eq!(pubkey.len(), 32, "controller LTPK is 32 bytes");
495 assert_eq!(sig.len(), 64, "controller signature is 64 bytes");
496
497 let mut ios_device_x = [0u8; 32];
498 hkdf_sha512(
499 &session_key,
500 CONTROLLER_SIGN_SALT,
501 CONTROLLER_SIGN_INFO,
502 &mut ios_device_x,
503 )
504 .unwrap();
505
506 let mut signed = Vec::new();
507 signed.extend_from_slice(&ios_device_x);
508 signed.extend_from_slice(id);
509 signed.extend_from_slice(pubkey);
510
511 let ltpk: [u8; 32] = pubkey.try_into().unwrap();
512 let signature: [u8; 64] = sig.try_into().unwrap();
513 verify_ed25519(<pk, &signed, &signature)
514 .expect("captured M5 controller signature must verify");
515 }
516
517 struct TestAccessory {
522 group: SrpGroup,
523 pairing_id: String,
524 signing: ed25519_dalek::SigningKey,
525 salt: Vec<u8>,
526 b_priv: BigUint,
527 b_pub: BigUint,
528 session_key: Option<Vec<u8>>,
529 a_pub: Option<BigUint>,
530 }
531
532 impl TestAccessory {
533 fn new(password: &str) -> Self {
534 let group = hap_group().unwrap();
535 let salt = vec![0x11u8; 16];
536 let x = compute_x::<Sha512>(&salt, PAIR_SETUP_USERNAME, password.as_bytes());
537 let v = compute_v(&group, &x);
538 let k = compute_k::<Sha512>(&group);
539 let b_priv = BigUint::from_bytes_be(&[0x5Au8; 32]);
540 let b_pub = compute_b(&group, &k, &v, &b_priv);
541 let signing = ed25519_dalek::SigningKey::from_bytes(&[0x99u8; 32]);
542 Self {
543 group,
544 pairing_id: "11:22:33:44:55:66".to_string(),
545 signing,
546 salt,
547 b_priv,
548 b_pub,
549 session_key: None,
550 a_pub: None,
551 }
552 }
553
554 fn m2(&self) -> Vec<u8> {
557 let mut out = Vec::new();
558 let mut w = Tlv8Writer::new(&mut out);
559 w.push_u8(tlv::STATE, tlv::STATE_M2);
560 w.push(tlv::SALT, &self.salt);
561 w.push(tlv::PUBLIC_KEY, &pad_be(&self.b_pub, 384));
562 out
563 }
564
565 fn m4(&mut self, m3: &[u8]) -> Vec<u8> {
568 let map = Tlv8Map::parse(m3).unwrap();
569 let a_bytes = map.get(tlv::PUBLIC_KEY).unwrap();
570 let a_pub = BigUint::from_bytes_be(a_bytes);
571 let m1 = map.get(tlv::PROOF).unwrap().to_vec();
572
573 let modulus = self.group.modulus();
574 let scrambler = compute_u::<Sha512>(&self.group, &a_pub, &self.b_pub);
577 let x_priv =
578 compute_x::<Sha512>(&self.salt, PAIR_SETUP_USERNAME, TEST_PASSWORD.as_bytes());
579 let verifier = compute_v(&self.group, &x_priv);
580 let vu = verifier.modpow(&scrambler, modulus);
581 let base = (&a_pub * &vu) % modulus;
582 let premaster = base.modpow(&self.b_priv, modulus);
583 let session_key = Sha512::digest(pad_be(&premaster, 384)).to_vec();
584
585 let expected_m1 = self.controller_m1(&a_pub, &session_key);
588 assert_eq!(m1, expected_m1, "test accessory: controller M1 must verify");
589
590 self.a_pub = Some(a_pub);
591 self.session_key = Some(session_key.clone());
592
593 let m2_proof = {
594 let mut h = Sha512::new();
595 h.update(pad_be(self.a_pub.as_ref().unwrap(), 384));
596 h.update(&m1);
597 h.update(&session_key);
598 h.finalize().to_vec()
599 };
600
601 let mut out = Vec::new();
602 let mut w = Tlv8Writer::new(&mut out);
603 w.push_u8(tlv::STATE, tlv::STATE_M4);
604 w.push(tlv::PROOF, &m2_proof);
605 out
606 }
607
608 fn controller_m1(&self, a_pub: &BigUint, session_key: &[u8]) -> Vec<u8> {
609 let h_n = Sha512::digest(self.group.modulus().to_bytes_be());
610 let h_g = Sha512::digest(self.group.generator().to_bytes_be());
611 let h_xor: Vec<u8> = h_n.iter().zip(h_g.iter()).map(|(a, b)| a ^ b).collect();
612 let h_i = Sha512::digest(PAIR_SETUP_USERNAME);
613 let mut h = Sha512::new();
614 h.update(h_xor);
615 h.update(h_i);
616 h.update(&self.salt);
617 h.update(pad_be(a_pub, 384));
618 h.update(pad_be(&self.b_pub, 384));
619 h.update(session_key);
620 h.finalize().to_vec()
621 }
622
623 fn m6(&self, _m5: &[u8]) -> Vec<u8> {
626 let session_key = self.session_key.as_ref().unwrap();
627 let mut accessory_x = [0u8; 32];
628 hkdf_sha512(
629 session_key,
630 ACCESSORY_SIGN_SALT,
631 ACCESSORY_SIGN_INFO,
632 &mut accessory_x,
633 )
634 .unwrap();
635 let ltpk = self.signing.verifying_key().to_bytes();
636 let id = self.pairing_id.as_bytes();
637
638 let mut signed = Vec::new();
639 signed.extend_from_slice(&accessory_x);
640 signed.extend_from_slice(id);
641 signed.extend_from_slice(<pk);
642 let sig = self.signing.sign(&signed).to_bytes();
643
644 let mut sub = Vec::new();
645 let mut sw = Tlv8Writer::new(&mut sub);
646 sw.push(tlv::IDENTIFIER, id);
647 sw.push(tlv::PUBLIC_KEY, <pk);
648 sw.push(tlv::SIGNATURE, &sig);
649
650 let mut enc_key = [0u8; 32];
651 hkdf_sha512(session_key, ENCRYPT_SALT, ENCRYPT_INFO, &mut enc_key).unwrap();
652 let sealed = encrypt(&enc_key, &hap_nonce(NONCE_M6), b"", &sub).unwrap();
653
654 let mut out = Vec::new();
655 let mut w = Tlv8Writer::new(&mut out);
656 w.push_u8(tlv::STATE, tlv::STATE_M6);
657 w.push(tlv::ENCRYPTED_DATA, &sealed);
658 out
659 }
660 }
661
662 const TEST_PASSWORD: &str = "123-45-678";
663
664 fn pad_be(v: &BigUint, width: usize) -> Vec<u8> {
666 let raw = v.to_bytes_be();
667 if raw.len() >= width {
668 return raw;
669 }
670 let mut out = vec![0u8; width - raw.len()];
671 out.extend_from_slice(&raw);
672 out
673 }
674
675 #[test]
676 fn full_machine_replay_reaches_done() {
677 let mut accessory = TestAccessory::new(TEST_PASSWORD);
678 let a = [0x37u8; 32];
679 let mut client =
680 PairSetupClient::new_with_private(TEST_PASSWORD, test_controller(), &a).unwrap();
681
682 let m1 = client.start();
683 assert_eq!(
684 Tlv8Map::parse(&m1).unwrap().get_u8(tlv::STATE).unwrap(),
685 Some(tlv::STATE_M1)
686 );
687
688 let m2 = accessory.m2();
690 let PairSetupStep::Send(m3) = client.handle(&m2).unwrap() else {
691 panic!("expected M3");
692 };
693
694 let m4 = accessory.m4(&m3);
696 let PairSetupStep::Send(m5) = client.handle(&m4).unwrap() else {
697 panic!("expected M5");
698 };
699
700 let m6 = accessory.m6(&m5);
702 let PairSetupStep::Done(pairing) = client.handle(&m6).unwrap() else {
703 panic!("expected Done");
704 };
705 assert_eq!(pairing.pairing_id, "11:22:33:44:55:66");
706 assert_eq!(pairing.ltpk, accessory.signing.verifying_key().to_bytes());
707 }
708
709 #[test]
710 fn wrong_setup_code_fails_m4_proof() {
711 let accessory = TestAccessory::new(TEST_PASSWORD);
712 let a = [0x37u8; 32];
713 let mut client =
716 PairSetupClient::new_with_private("999-99-999", test_controller(), &a).unwrap();
717 let _ = client.start();
718 let m2 = accessory.m2();
719 let PairSetupStep::Send(m3) = client.handle(&m2).unwrap() else {
720 panic!("expected M3");
721 };
722 let _ = m3;
726 let mut bad_m4 = Vec::new();
727 let mut w = Tlv8Writer::new(&mut bad_m4);
728 w.push_u8(tlv::STATE, tlv::STATE_M4);
729 w.push(tlv::PROOF, &[0u8; 64]);
730 assert!(matches!(
731 client.handle(&bad_m4),
732 Err(CryptoError::SrpProofMismatch)
733 ));
734 }
735
736 #[test]
737 fn accessory_error_tlv_is_surfaced() {
738 let a = [0x37u8; 32];
739 let mut client =
740 PairSetupClient::new_with_private(TEST_PASSWORD, test_controller(), &a).unwrap();
741 let _ = client.start();
742 let mut err = Vec::new();
744 let mut w = Tlv8Writer::new(&mut err);
745 w.push_u8(tlv::STATE, tlv::STATE_M2);
746 w.push_u8(tlv::ERROR, 2); assert!(matches!(
748 client.handle(&err),
749 Err(CryptoError::SrpProofMismatch)
750 ));
751 }
752
753 #[test]
754 fn handle_before_start_errors() {
755 let a = [0x37u8; 32];
756 let mut client =
757 PairSetupClient::new_with_private(TEST_PASSWORD, test_controller(), &a).unwrap();
758 assert!(client.handle(b"").is_err());
759 }
760
761 #[test]
762 fn normalize_setup_code_regroups_bare_digits() {
763 assert_eq!(normalize_setup_code("12345678"), "123-45-678");
764 assert_eq!(normalize_setup_code("123-45-678"), "123-45-678");
765 assert_eq!(normalize_setup_code("oddball"), "oddball");
766 }
767}