1#![forbid(unsafe_code)]
4
5use std::sync::Arc;
6
7use matter_cert::time::MatterTime;
8
9use crate::attestation::PaaTrustStore;
10use crate::noc::{FabricRecord, NocRng};
11use crate::setup::SetupPayload;
12use crate::state_machine::action::{Action, Expectation};
13use crate::state_machine::error::CommissioningError;
14use crate::state_machine::stage::Stage;
15
16#[cfg(feature = "tracing")]
17use tracing::instrument;
18
19pub(crate) const DEFAULT_CONNECT_MAX_TIME_SECONDS: u16 = 90;
26
27#[derive(Clone, PartialEq, Eq)]
37pub struct WiFiCredentials {
38 pub ssid: Vec<u8>,
40 pub credentials: Vec<u8>,
43}
44
45impl core::fmt::Debug for WiFiCredentials {
46 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
47 f.debug_struct("WiFiCredentials")
48 .field("ssid", &format_args!("<{} bytes>", self.ssid.len()))
49 .field(
50 "credentials",
51 &format_args!("<redacted, {} bytes>", self.credentials.len()),
52 )
53 .finish()
54 }
55}
56
57#[derive(Clone, Debug, PartialEq, Eq)]
75pub enum NetworkCredentials {
76 WiFi(WiFiCredentials),
78 Thread(crate::thread_dataset::ThreadDataset),
80 AlreadyOnNetwork,
82}
83
84pub struct CommissionerConfig<'a> {
96 pub pase_attestation_challenge: [u8; 16],
101 pub fabric: &'a FabricRecord,
104 pub setup_payload: &'a SetupPayload,
108 pub paa_trust_store: &'a PaaTrustStore,
110 pub cd_signing_roots: &'a crate::attestation::CdSigningRoots,
114 pub commissioner_node_id: u64,
117 pub assigned_node_id: u64,
121 pub ipk_epoch_key: [u8; 16],
125 pub case_admin_subject: u64,
128 pub admin_vendor_id: u16,
130 pub now: MatterTime,
134 pub rng: Arc<dyn NocRng>,
136 pub network: NetworkCredentials,
147}
148
149#[allow(clippy::struct_field_names)]
158pub struct Commissioner {
159 stage: Stage,
160
161 #[allow(dead_code)] pase_attestation_challenge: [u8; 16],
168 #[allow(dead_code)] fabric: FabricRecord,
170 #[allow(dead_code)] paa_trust_store: PaaTrustStore,
172 cd_signing_roots: crate::attestation::CdSigningRoots,
173 #[allow(dead_code)] setup_payload: SetupPayload,
175 #[allow(dead_code)] commissioner_node_id: u64,
177 #[allow(dead_code)] assigned_node_id: u64,
179 #[allow(dead_code)] ipk_epoch_key: [u8; 16],
181 #[allow(dead_code)] case_admin_subject: u64,
183 #[allow(dead_code)] admin_vendor_id: u16,
185 #[allow(dead_code)] now: MatterTime,
187 #[allow(dead_code)] rng: Arc<dyn NocRng>,
189
190 pai_der: Option<Vec<u8>>,
194 dac_der: Option<Vec<u8>>,
195 attestation_nonce: Option<[u8; 32]>,
196 attestation_response: Option<crate::attestation::AttestationResponse>,
197
198 csr_nonce: Option<[u8; 32]>,
202 csr_response: Option<crate::noc::CsrResponse>,
203 verified_csr: Option<crate::noc::VerifiedCsr>,
204 issued_noc: Option<matter_cert::MatterCertificate>,
205 issued_noc_public_key: Option<[u8; 65]>,
206
207 network: NetworkCredentials,
213
214 failsafe_expiry_seconds: u16,
220
221 connect_max_time_seconds: u16,
230
231 breadcrumb_counter: u64,
237
238 awaiting_case_session: bool,
243
244 awaiting: Option<Expectation>,
248
249 pending_action: Option<Action>,
252
253 last_failure: Option<String>,
258}
259
260impl Commissioner {
261 pub fn new(cfg: CommissionerConfig<'_>) -> Result<Self, CommissioningError> {
270 if cfg.commissioner_node_id == 0 {
271 return Err(CommissioningError::InvalidConfig(
272 "commissioner_node_id must be non-zero",
273 ));
274 }
275 if cfg.assigned_node_id == 0 {
276 return Err(CommissioningError::InvalidConfig(
277 "assigned_node_id must be non-zero",
278 ));
279 }
280 if cfg.assigned_node_id == cfg.commissioner_node_id {
281 return Err(CommissioningError::InvalidConfig(
282 "assigned_node_id must differ from commissioner_node_id",
283 ));
284 }
285 if cfg.ipk_epoch_key == [0u8; 16] {
286 return Err(CommissioningError::InvalidConfig(
287 "ipk_epoch_key must not be all-zero",
288 ));
289 }
290 if let NetworkCredentials::WiFi(creds) = &cfg.network {
294 if creds.ssid.is_empty() {
295 return Err(CommissioningError::InvalidConfig(
296 "network: Wi-Fi ssid must not be empty",
297 ));
298 }
299 if creds.ssid.len() > 32 {
300 return Err(CommissioningError::InvalidConfig(
301 "network: Wi-Fi ssid must be ≤32 bytes",
302 ));
303 }
304 if creds.credentials.len() > 64 {
305 return Err(CommissioningError::InvalidConfig(
306 "network: Wi-Fi credentials must be ≤64 bytes",
307 ));
308 }
309 }
310 Ok(Self {
311 stage: Stage::SecurePairing,
312 pase_attestation_challenge: cfg.pase_attestation_challenge,
313 fabric: cfg.fabric.clone(),
314 paa_trust_store: cfg.paa_trust_store.clone(),
315 cd_signing_roots: cfg.cd_signing_roots.clone(),
316 setup_payload: cfg.setup_payload.clone(),
317 commissioner_node_id: cfg.commissioner_node_id,
318 assigned_node_id: cfg.assigned_node_id,
319 ipk_epoch_key: cfg.ipk_epoch_key,
320 case_admin_subject: cfg.case_admin_subject,
321 admin_vendor_id: cfg.admin_vendor_id,
322 now: cfg.now,
323 rng: cfg.rng,
324 pai_der: None,
325 dac_der: None,
326 attestation_nonce: None,
327 attestation_response: None,
328 csr_nonce: None,
329 csr_response: None,
330 verified_csr: None,
331 issued_noc: None,
332 issued_noc_public_key: None,
333 network: cfg.network,
334 failsafe_expiry_seconds: 60,
335 connect_max_time_seconds: 0,
336 breadcrumb_counter: 1,
337 awaiting_case_session: false,
338 awaiting: None,
339 pending_action: None,
340 last_failure: None,
341 })
342 }
343
344 #[must_use]
346 pub fn stage(&self) -> Stage {
347 self.stage
348 }
349
350 #[allow(dead_code)] pub(crate) fn network(&self) -> &NetworkCredentials {
355 &self.network
356 }
357
358 #[cfg(feature = "__test_shortcuts")]
369 #[must_use]
370 pub fn position_at_stage_for_test(mut self, stage: Stage, seeds: TestStateSeeds) -> Self {
371 self.stage = stage;
372 if let Some(pk) = seeds.synthetic_noc_pubkey {
373 self.issued_noc_public_key = Some(pk);
374 }
375 self
376 }
377
378 #[cfg_attr(feature = "tracing", instrument(skip(self), fields(stage = ?self.stage)))]
391 pub fn poll(&mut self) -> Result<Action, CommissioningError> {
392 if let Some(act) = self.pending_action.clone() {
393 return Ok(act);
394 }
395 let action = self.dispatch_stage()?;
396 self.pending_action = Some(action.clone());
397 Ok(action)
398 }
399
400 fn network_enable_failsafe_seconds(&self) -> u16 {
407 if self.connect_max_time_seconds > 0 {
408 self.connect_max_time_seconds
409 } else {
410 DEFAULT_CONNECT_MAX_TIME_SECONDS
411 }
412 }
413
414 pub(crate) fn set_connect_max_time_seconds(&mut self, seconds: u16) {
422 self.connect_max_time_seconds = seconds;
423 }
424
425 #[must_use]
435 pub(crate) fn connect_max_time_seconds(&self) -> u16 {
436 self.connect_max_time_seconds
437 }
438
439 fn arm_failsafe_action(&mut self) -> Action {
447 use crate::clusters::general_commissioning as gc;
448 use crate::state_machine::action::SessionContext;
449 let expiry_seconds = if self.stage == Stage::FailsafeBeforeNetworkEnable {
450 self.network_enable_failsafe_seconds()
451 } else {
452 self.failsafe_expiry_seconds
453 };
454 let breadcrumb = self.next_breadcrumb();
455 let payload = gc::encode_arm_fail_safe(expiry_seconds, breadcrumb);
456 self.awaiting = Some(Expectation::ArmFailsafeResponse);
457 Action::Invoke {
458 session: SessionContext::Pase,
459 endpoint: 0,
460 cluster: gc::CLUSTER_ID,
461 command: gc::command_id::ARM_FAIL_SAFE,
462 payload,
463 expect: Expectation::ArmFailsafeResponse,
464 }
465 }
466
467 #[allow(clippy::too_many_lines)]
479 fn dispatch_stage(&mut self) -> Result<Action, CommissioningError> {
480 use crate::clusters::general_commissioning as gc;
481 use crate::state_machine::action::SessionContext;
482 match self.stage {
483 Stage::SecurePairing => {
484 self.stage = Stage::ReadCommissioningInfo;
486 self.dispatch_stage()
487 }
488 Stage::ReadCommissioningInfo => {
489 self.awaiting = Some(Expectation::CommissioningInfo);
490 Ok(Action::ReadAttribute {
491 session: SessionContext::Pase,
492 endpoint: 0,
493 cluster: gc::CLUSTER_ID,
494 attributes: &[
495 0x0000, 0x0001, 0x0002, 0x0004, ],
502 expect: Expectation::CommissioningInfo,
503 })
504 }
505 Stage::ArmFailsafe | Stage::FailsafeBeforeNetworkEnable => {
506 Ok(self.arm_failsafe_action())
507 }
508 Stage::ConfigRegulatory => {
509 let breadcrumb = self.next_breadcrumb();
510 let payload = gc::encode_set_regulatory_config(
517 gc::RegulatoryLocation::Indoor,
518 "XX",
519 breadcrumb,
520 );
521 self.awaiting = Some(Expectation::SetRegulatoryConfigResponse);
522 Ok(Action::Invoke {
523 session: SessionContext::Pase,
524 endpoint: 0,
525 cluster: gc::CLUSTER_ID,
526 command: gc::command_id::SET_REGULATORY_CONFIG,
527 payload,
528 expect: Expectation::SetRegulatoryConfigResponse,
529 })
530 }
531 Stage::SendPaiCertRequest => {
532 use crate::noc::{encode_certificate_chain_request, CertChainType};
533 let payload = encode_certificate_chain_request(CertChainType::Pai);
534 self.awaiting = Some(Expectation::PaiCertChainResponse);
535 Ok(Action::Invoke {
536 session: SessionContext::Pase,
537 endpoint: 0,
538 cluster: 0x003E,
539 command: 0x02,
540 payload,
541 expect: Expectation::PaiCertChainResponse,
542 })
543 }
544 Stage::SendDacCertRequest => {
545 use crate::noc::{encode_certificate_chain_request, CertChainType};
546 let payload = encode_certificate_chain_request(CertChainType::Dac);
547 self.awaiting = Some(Expectation::DacCertChainResponse);
548 Ok(Action::Invoke {
549 session: SessionContext::Pase,
550 endpoint: 0,
551 cluster: 0x003E,
552 command: 0x02,
553 payload,
554 expect: Expectation::DacCertChainResponse,
555 })
556 }
557 Stage::SendAttestationRequest => {
558 use crate::noc::encode_attestation_request;
559 let mut nonce = [0u8; 32];
560 self.rng
561 .fill(&mut nonce)
562 .map_err(CommissioningError::from)?;
563 let payload = encode_attestation_request(&nonce);
564 self.attestation_nonce = Some(nonce);
565 self.awaiting = Some(Expectation::AttestationResponse);
566 Ok(Action::Invoke {
567 session: SessionContext::Pase,
568 endpoint: 0,
569 cluster: 0x003E,
570 command: 0x00,
571 payload,
572 expect: Expectation::AttestationResponse,
573 })
574 }
575 Stage::AttestationVerification => {
576 match self.run_attestation_verification() {
577 Ok(()) => {
578 self.advance(Stage::SendOpCertSigningRequest);
579 self.dispatch_stage()
580 }
581 Err(err) => {
582 self.last_failure = Some(err.to_string());
586 self.stage = Stage::Failed;
587 self.awaiting = None;
588 self.pending_action = None;
589 Err(err)
590 }
591 }
592 }
593 Stage::SendOpCertSigningRequest => {
594 use crate::noc::encode_csr_request;
595 let mut nonce = [0u8; 32];
596 self.rng
597 .fill(&mut nonce)
598 .map_err(CommissioningError::from)?;
599 let payload = encode_csr_request(&nonce, false);
602 self.csr_nonce = Some(nonce);
603 self.awaiting = Some(Expectation::CsrResponse);
604 Ok(Action::Invoke {
605 session: SessionContext::Pase,
606 endpoint: 0,
607 cluster: 0x003E,
608 command: 0x04,
609 payload,
610 expect: Expectation::CsrResponse,
611 })
612 }
613 Stage::ValidateCsr => {
614 self.run_validate_csr()?;
616 self.advance(Stage::GenerateNocChain);
617 self.dispatch_stage()
618 }
619 Stage::GenerateNocChain => {
620 self.run_generate_noc_chain()?;
622 self.advance(Stage::SendTrustedRootCert);
623 self.dispatch_stage()
624 }
625 Stage::SendTrustedRootCert => {
626 use crate::noc::encode_add_trusted_root;
627 let rcac_tlv =
634 self.fabric.root_cert.to_tlv().map_err(|e| {
635 CommissioningError::from(crate::noc::NocError::CertBuild(e))
636 })?;
637 #[cfg(feature = "tracing")]
638 tracing::debug!(
639 rcac_tlv = %crate::hexdump::hex(&rcac_tlv),
640 "sending AddTrustedRootCertificate"
641 );
642 let payload = encode_add_trusted_root(&rcac_tlv);
643 self.awaiting = Some(Expectation::AddTrustedRootResponse);
644 Ok(Action::Invoke {
645 session: SessionContext::Pase,
646 endpoint: 0,
647 cluster: 0x003E,
648 command: 0x0B,
649 payload,
650 expect: Expectation::AddTrustedRootResponse,
651 })
652 }
653 Stage::SendNoc => {
654 use crate::noc::encode_add_noc;
655 let noc = self
656 .issued_noc
657 .as_ref()
658 .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
659 let noc_tlv = noc
660 .to_tlv()
661 .map_err(|e| CommissioningError::from(crate::noc::NocError::CertBuild(e)))?;
662 let payload = encode_add_noc(
665 &noc_tlv,
666 None,
667 &self.ipk_epoch_key,
668 self.case_admin_subject,
669 self.admin_vendor_id,
670 );
671 self.awaiting = Some(Expectation::NocResponse);
672 Ok(Action::Invoke {
673 session: SessionContext::Pase,
674 endpoint: 0,
675 cluster: 0x003E,
676 command: 0x06,
677 payload,
678 expect: Expectation::NocResponse,
679 })
680 }
681 Stage::ReadNetworkCommissioningInfo => {
682 self.awaiting = Some(Expectation::NetworkCommissioningInfo);
683 Ok(Action::ReadAttribute {
684 session: SessionContext::Pase,
685 endpoint: 0,
686 cluster: crate::clusters::network_commissioning::CLUSTER_ID,
687 attributes: &[
688 crate::clusters::network_commissioning::attribute_id::FEATURE_MAP,
689 crate::clusters::network_commissioning::attribute_id::CONNECT_MAX_TIME_SECONDS,
693 ],
694 expect: Expectation::NetworkCommissioningInfo,
695 })
696 }
697 Stage::NetworkSetup => {
698 use crate::clusters::network_commissioning as nc;
699 let breadcrumb = self.next_breadcrumb();
707 let (command, payload) = match &self.network {
708 NetworkCredentials::WiFi(creds) => (
709 nc::command_id::ADD_OR_UPDATE_WIFI_NETWORK,
710 nc::encode_add_or_update_wifi_network(
711 &creds.ssid,
712 &creds.credentials,
713 breadcrumb,
714 ),
715 ),
716 NetworkCredentials::Thread(dataset) => (
717 nc::command_id::ADD_OR_UPDATE_THREAD_NETWORK,
718 nc::encode_add_or_update_thread_network(dataset.as_bytes(), breadcrumb),
719 ),
720 NetworkCredentials::AlreadyOnNetwork => {
721 return Err(CommissioningError::OutOfOrderResponse(self.stage));
722 }
723 };
724 self.awaiting = Some(Expectation::NetworkConfigResponse);
725 Ok(Action::Invoke {
726 session: SessionContext::Pase,
727 endpoint: 0,
728 cluster: nc::CLUSTER_ID,
729 command,
730 payload,
731 expect: Expectation::NetworkConfigResponse,
732 })
733 }
734 Stage::NetworkEnable => {
735 use crate::clusters::network_commissioning as nc;
736 let breadcrumb = self.next_breadcrumb();
739 let payload = match &self.network {
740 NetworkCredentials::WiFi(creds) => {
741 nc::encode_connect_network(&creds.ssid, breadcrumb)
742 }
743 NetworkCredentials::Thread(dataset) => {
744 nc::encode_connect_network(&dataset.ext_pan_id(), breadcrumb)
745 }
746 NetworkCredentials::AlreadyOnNetwork => {
747 return Err(CommissioningError::OutOfOrderResponse(self.stage));
748 }
749 };
750 self.awaiting = Some(Expectation::ConnectNetworkResponse);
751 Ok(Action::Invoke {
752 session: SessionContext::Pase,
753 endpoint: 0,
754 cluster: nc::CLUSTER_ID,
755 command: nc::command_id::CONNECT_NETWORK,
756 payload,
757 expect: Expectation::ConnectNetworkResponse,
758 })
759 }
760 Stage::EvictPreviousCaseSessions => {
761 self.advance(Stage::FindOperationalForComplete);
765 self.dispatch_stage()
766 }
767 Stage::FindOperationalForComplete => {
768 self.awaiting_case_session = true;
769 Ok(Action::EstablishCase {
770 fabric_id: self.fabric.fabric_id,
771 peer_node_id: self.assigned_node_id,
772 })
773 }
774 Stage::SendComplete => {
775 let payload = gc::encode_commissioning_complete();
776 self.awaiting = Some(Expectation::CommissioningCompleteResponse);
777 Ok(Action::Invoke {
778 session: SessionContext::Case,
779 endpoint: 0,
780 cluster: gc::CLUSTER_ID,
781 command: gc::command_id::COMMISSIONING_COMPLETE,
782 payload,
783 expect: Expectation::CommissioningCompleteResponse,
784 })
785 }
786 Stage::Cleanup => {
787 let public_key = self
788 .issued_noc_public_key
789 .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
790 Ok(Action::Done(crate::state_machine::CommissionedFabric {
791 fabric: self.fabric.clone(),
792 peer_node_id: self.assigned_node_id,
793 peer_root_public_key: public_key,
794 terminated_at: Stage::Cleanup,
795 }))
796 }
797 Stage::Failed => {
798 self.awaiting = None;
801 let reason = self
802 .last_failure
803 .clone()
804 .unwrap_or_else(|| "commissioning aborted".to_string());
805 Ok(Action::Abort {
806 send_disarm_failsafe: true,
807 reason,
808 })
809 } }
813 }
814
815 #[cfg_attr(feature = "tracing", instrument(skip(self, payload), fields(stage = ?self.stage, expectation = ?expect)))]
837 pub fn on_response(
838 &mut self,
839 expect: Expectation,
840 payload: &[u8],
841 ) -> Result<(), CommissioningError> {
842 if expect == Expectation::CaseFailed {
843 if !self.awaiting_case_session {
848 return Err(CommissioningError::OutOfOrderResponse(self.stage));
849 }
850 self.awaiting_case_session = false;
851 self.stage = Stage::Failed;
852 self.awaiting = None;
853 self.pending_action = None;
854 self.last_failure = Some(CommissioningError::CaseEstablishmentFailed.to_string());
855 return Err(CommissioningError::CaseEstablishmentFailed);
856 }
857 let Some(awaiting) = self.awaiting else {
858 return Err(CommissioningError::OutOfOrderResponse(self.stage));
859 };
860 if awaiting != expect {
861 return Err(CommissioningError::UnexpectedResponseKind {
862 expected: awaiting,
863 got: expect,
864 });
865 }
866 match self.handle_response(expect, payload) {
867 Ok(()) => Ok(()),
868 Err(err) => {
869 self.last_failure = Some(err.to_string());
870 self.stage = Stage::Failed;
871 self.awaiting = None;
872 self.pending_action = None;
873 Err(err)
874 }
875 }
876 }
877
878 #[cfg_attr(feature = "tracing", instrument(skip(self)))]
890 pub fn on_case_established(&mut self) -> Result<(), CommissioningError> {
891 if !self.awaiting_case_session {
892 return Err(CommissioningError::OutOfOrderResponse(self.stage));
893 }
894 self.awaiting_case_session = false;
895 self.advance(Stage::SendComplete);
896 Ok(())
897 }
898
899 #[allow(clippy::too_many_lines)]
900 fn handle_response(
901 &mut self,
902 expect: Expectation,
903 payload: &[u8],
904 ) -> Result<(), CommissioningError> {
905 use crate::clusters::general_commissioning as gc;
906 match expect {
907 Expectation::CommissioningInfo => {
908 Self::assert_tlv_well_formed(self.stage, payload)?;
909 if let Some(info) = gc::decode_basic_commissioning_info(payload) {
913 if info.failsafe_expiry_length_seconds > 0 {
914 self.failsafe_expiry_seconds = info.failsafe_expiry_length_seconds;
915 }
916 }
917 self.advance(Stage::ArmFailsafe);
918 Ok(())
919 }
920 Expectation::ArmFailsafeResponse => {
921 let resp = gc::decode_arm_fail_safe_response(payload)?;
922 if resp.error_code != 0 {
923 return Err(CommissioningError::DeviceImStatus {
924 stage: self.stage,
925 im_status: u16::from(resp.error_code),
926 });
927 }
928 let next = match self.stage {
929 Stage::ArmFailsafe => Stage::ConfigRegulatory,
930 Stage::FailsafeBeforeNetworkEnable => Stage::NetworkEnable,
931 other => {
932 return Err(CommissioningError::OutOfOrderResponse(other));
933 }
934 };
935 self.advance(next);
936 Ok(())
937 }
938 Expectation::SetRegulatoryConfigResponse => {
939 let resp = gc::decode_set_regulatory_config_response(payload)?;
940 if resp.error_code != 0 {
941 return Err(CommissioningError::DeviceImStatus {
942 stage: Stage::ConfigRegulatory,
943 im_status: u16::from(resp.error_code),
944 });
945 }
946 self.advance(Stage::SendPaiCertRequest);
947 Ok(())
948 }
949 Expectation::PaiCertChainResponse => {
950 let resp = crate::noc::decode_certificate_chain_response(payload)?;
951 self.pai_der = Some(resp.certificate);
952 self.advance(Stage::SendDacCertRequest);
953 Ok(())
954 }
955 Expectation::DacCertChainResponse => {
956 let resp = crate::noc::decode_certificate_chain_response(payload)?;
957 self.dac_der = Some(resp.certificate);
958 self.advance(Stage::SendAttestationRequest);
959 Ok(())
960 }
961 Expectation::AttestationResponse => {
962 let resp = crate::noc::decode_attestation_response(payload)?;
963 self.attestation_response = Some(resp);
964 self.advance(Stage::AttestationVerification);
965 Ok(())
966 }
967 Expectation::CsrResponse => {
968 let resp = crate::noc::decode_csr_response(payload)?;
969 self.csr_response = Some(resp);
970 self.advance(Stage::ValidateCsr);
971 Ok(())
972 }
973 Expectation::AddTrustedRootResponse => {
974 if payload.first() != Some(&0u8) {
979 return Err(CommissioningError::DeviceImStatus {
980 stage: Stage::SendTrustedRootCert,
981 im_status: u16::from(payload.first().copied().unwrap_or(0xFF)),
982 });
983 }
984 self.advance(Stage::SendNoc);
985 Ok(())
986 }
987 Expectation::NocResponse => {
988 let resp = crate::noc::decode_noc_response(payload)?;
989 if resp.status != 0 {
990 return Err(CommissioningError::DeviceImStatus {
991 stage: Stage::SendNoc,
992 im_status: u16::from(resp.status),
993 });
994 }
995 self.advance(Stage::ReadNetworkCommissioningInfo);
996 Ok(())
997 }
998 Expectation::NetworkCommissioningInfo => {
999 use crate::clusters::network_commissioning as nc;
1000 use crate::state_machine::NetworkKind;
1001 let features = nc::decode_feature_map(payload)?;
1002 if features.is_empty() {
1007 return Err(CommissioningError::MalformedResponse(
1008 Stage::ReadNetworkCommissioningInfo,
1009 ));
1010 }
1011 match &self.network {
1019 NetworkCredentials::WiFi(_) => {
1020 if !features.contains(nc::NetworkCommissioningFeature::WIFI) {
1021 return Err(CommissioningError::NetworkFeatureUnsupported {
1022 needed: NetworkKind::WiFi,
1023 });
1024 }
1025 self.advance(Stage::NetworkSetup);
1026 }
1027 NetworkCredentials::Thread(_) => {
1028 if !features.contains(nc::NetworkCommissioningFeature::THREAD) {
1029 return Err(CommissioningError::NetworkFeatureUnsupported {
1030 needed: NetworkKind::Thread,
1031 });
1032 }
1033 self.advance(Stage::NetworkSetup);
1034 }
1035 NetworkCredentials::AlreadyOnNetwork => {
1036 self.advance(Stage::EvictPreviousCaseSessions);
1045 }
1046 }
1047 Ok(())
1048 }
1049 Expectation::NetworkConfigResponse => {
1050 use crate::clusters::network_commissioning as nc;
1051 let resp = nc::decode_network_config_response(Stage::NetworkSetup, payload)?;
1052 if resp.networking_status != 0 {
1053 return Err(CommissioningError::NetworkRejected {
1054 stage: Stage::NetworkSetup,
1055 networking_status: resp.networking_status,
1056 debug_text: resp.debug_text,
1057 remediation_hint: nc::remediation_for(resp.networking_status),
1058 });
1059 }
1060 self.advance(Stage::FailsafeBeforeNetworkEnable);
1061 Ok(())
1062 }
1063 Expectation::ConnectNetworkResponse => {
1064 use crate::clusters::network_commissioning as nc;
1065 let resp = nc::decode_connect_network_response(Stage::NetworkEnable, payload)?;
1066 if resp.networking_status != 0 {
1067 return Err(CommissioningError::NetworkRejected {
1068 stage: Stage::NetworkEnable,
1069 networking_status: resp.networking_status,
1070 debug_text: resp.debug_text,
1071 remediation_hint: nc::remediation_for(resp.networking_status),
1072 });
1073 }
1074 self.advance(Stage::EvictPreviousCaseSessions);
1075 Ok(())
1076 }
1077 Expectation::CommissioningCompleteResponse => {
1078 let (error_code, _debug) =
1079 gc::decode_commissioning_error_response(Stage::SendComplete, payload)?;
1080 if error_code != 0 {
1081 return Err(CommissioningError::DeviceImStatus {
1082 stage: Stage::SendComplete,
1083 im_status: u16::from(error_code),
1084 });
1085 }
1086 self.advance(Stage::Cleanup);
1087 Ok(())
1088 }
1089 _ => Err(CommissioningError::OutOfOrderResponse(self.stage)),
1092 }
1093 }
1094
1095 fn next_breadcrumb(&mut self) -> u64 {
1096 let b = self.breadcrumb_counter;
1097 self.breadcrumb_counter = b.saturating_add(1);
1098 b
1099 }
1100
1101 fn advance(&mut self, next: Stage) {
1102 self.stage = next;
1103 self.awaiting = None;
1104 self.pending_action = None;
1105 }
1106
1107 fn run_attestation_verification(&mut self) -> Result<(), CommissioningError> {
1127 use crate::attestation::{
1128 extract_attestation_elements_fields, verify_attestation_response, verify_chain,
1129 AttestationError, Dac, Pai,
1130 };
1131
1132 let pai_der = self
1133 .pai_der
1134 .as_ref()
1135 .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1136 let dac_der = self
1137 .dac_der
1138 .as_ref()
1139 .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1140 let response = self
1141 .attestation_response
1142 .as_ref()
1143 .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1144 let expected_nonce = self
1145 .attestation_nonce
1146 .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1147
1148 let pai = Pai::from_der(pai_der)?;
1150 let dac = Dac::from_der(dac_der)?;
1151 #[cfg(feature = "tracing")]
1152 tracing::debug!(
1153 dac_der = %crate::hexdump::hex(dac_der),
1154 pai_der = %crate::hexdump::hex(pai_der),
1155 "verifying attestation chain"
1156 );
1157
1158 let chain = verify_chain(&dac, &pai, &self.paa_trust_store, self.now)?;
1165
1166 verify_attestation_response(response, &self.pase_attestation_challenge, dac.public_key())?;
1168
1169 let fields = extract_attestation_elements_fields(&response.attestation_elements)?;
1172 if fields.attestation_nonce != expected_nonce {
1173 return Err(CommissioningError::Attestation(
1174 AttestationError::ResponseElementsMalformed,
1175 ));
1176 }
1177
1178 #[cfg(feature = "tracing")]
1182 tracing::debug!(
1183 cd_cms = %crate::hexdump::hex(&fields.certification_declaration),
1184 "verifying certification declaration"
1185 );
1186 crate::attestation::verify_certification_declaration(
1187 &fields.certification_declaration,
1188 chain.vendor_id,
1189 chain.product_id,
1190 &self.cd_signing_roots,
1191 )?;
1192 Ok(())
1193 }
1194
1195 fn run_validate_csr(&mut self) -> Result<(), CommissioningError> {
1206 use crate::attestation::Dac;
1207 use crate::noc::verify_csr_response;
1208
1209 let resp = self
1210 .csr_response
1211 .as_ref()
1212 .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1213 let dac_der = self
1214 .dac_der
1215 .as_ref()
1216 .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1217 let csr_nonce = self
1218 .csr_nonce
1219 .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1220
1221 let dac = Dac::from_der(dac_der)?;
1222 let verified = verify_csr_response(
1223 &resp.nocsr_elements,
1224 &resp.attestation_signature,
1225 &csr_nonce,
1226 &self.pase_attestation_challenge,
1227 dac.public_key(),
1228 )?;
1229 self.verified_csr = Some(verified);
1230 Ok(())
1231 }
1232
1233 fn run_generate_noc_chain(&mut self) -> Result<(), CommissioningError> {
1247 use crate::noc::issue_noc;
1248
1249 let verified = self
1250 .verified_csr
1251 .as_ref()
1252 .ok_or(CommissioningError::OutOfOrderResponse(self.stage))?;
1253 let noc = issue_noc(
1254 &self.fabric,
1255 verified,
1256 self.assigned_node_id,
1257 &[],
1258 (self.now, MatterTime::NO_EXPIRY),
1259 self.rng.as_ref(),
1260 )?;
1261 self.issued_noc_public_key = Some(*verified.public_key.as_bytes());
1266 self.issued_noc = Some(noc);
1267 Ok(())
1268 }
1269
1270 fn assert_tlv_well_formed(stage: Stage, payload: &[u8]) -> Result<(), CommissioningError> {
1271 use matter_codec::{ContainerKind, Element, Tag, TlvReader};
1272 let mut reader = TlvReader::new(payload);
1273 match reader
1274 .next()
1275 .map_err(|_| CommissioningError::MalformedResponse(stage))?
1276 {
1277 Some(Element::ContainerStart {
1278 tag: Tag::Anonymous,
1279 kind: ContainerKind::Structure,
1280 }) => {}
1281 _ => return Err(CommissioningError::MalformedResponse(stage)),
1282 }
1283 loop {
1285 match reader
1286 .next()
1287 .map_err(|_| CommissioningError::MalformedResponse(stage))?
1288 {
1289 None => return Err(CommissioningError::MalformedResponse(stage)),
1290 Some(Element::ContainerEnd) => return Ok(()),
1291 Some(_) => {}
1292 }
1293 }
1294 }
1295}
1296
1297#[cfg(feature = "__test_shortcuts")]
1302#[derive(Default, Debug, Clone, Copy)]
1303pub struct TestStateSeeds {
1304 pub synthetic_noc_pubkey: Option<[u8; 65]>,
1309}
1310
1311#[cfg(test)]
1312mod tests {
1313 #![allow(clippy::unwrap_used, clippy::expect_used)]
1315
1316 use super::*;
1317 use crate::attestation::CdSigningRoots;
1318 use crate::noc::{FabricRecord, NocRng, SystemNocRng};
1319 use crate::setup::{
1320 CommissioningFlow, DiscoveryCapabilities, Discriminator, Passcode, SetupPayload,
1321 };
1322 use crate::state_machine::{Action, Expectation};
1323 use crate::PaaTrustStore;
1324 use matter_cert::time::MatterTime;
1325 use matter_crypto::{RingSigner, Signer};
1326 use std::sync::Arc;
1327
1328 fn make_setup_payload() -> SetupPayload {
1329 SetupPayload {
1330 version: 0,
1331 vendor_id: Some(0xFFF1),
1332 product_id: Some(0x8000),
1333 commissioning_flow: CommissioningFlow::Standard,
1334 discovery_capabilities: DiscoveryCapabilities::ON_NETWORK,
1335 discriminator: Discriminator::new(0x0F00).expect("valid discriminator"),
1336 passcode: Passcode::new(20_202_021).expect("valid passcode"),
1337 }
1338 }
1339
1340 fn make_fabric_record() -> FabricRecord {
1341 let (signer, _pkcs8) = RingSigner::generate().unwrap();
1342 let signer: Arc<dyn Signer> = Arc::new(signer);
1343 FabricRecord::new_root_only(
1344 0x0000_0000_0000_0001,
1345 signer,
1346 MatterTime::from_unix_secs(1_704_067_200),
1347 MatterTime::from_unix_secs(1_735_689_600),
1348 42,
1349 &SystemNocRng,
1350 )
1351 .unwrap()
1352 }
1353
1354 fn base_config<'a>(
1355 fabric: &'a FabricRecord,
1356 setup: &'a SetupPayload,
1357 paa: &'a PaaTrustStore,
1358 cd: &'a crate::attestation::CdSigningRoots,
1359 rng: Arc<dyn NocRng>,
1360 ) -> CommissionerConfig<'a> {
1361 CommissionerConfig {
1362 pase_attestation_challenge: [0u8; 16],
1363 fabric,
1364 setup_payload: setup,
1365 paa_trust_store: paa,
1366 cd_signing_roots: cd,
1367 commissioner_node_id: 0x1,
1368 assigned_node_id: 0x2,
1369 ipk_epoch_key: [0x42_u8; 16],
1370 case_admin_subject: 0x1,
1371 admin_vendor_id: 0xFFF1,
1372 now: MatterTime::from_unix_secs(1_704_067_200),
1373 rng,
1374 network: NetworkCredentials::AlreadyOnNetwork,
1375 }
1376 }
1377
1378 #[test]
1379 fn new_rejects_zero_commissioner_node_id() {
1380 let fabric = make_fabric_record();
1381 let setup = make_setup_payload();
1382 let paa = PaaTrustStore::with_csa_test_roots();
1383 let cd = CdSigningRoots::with_csa_test_roots();
1384 let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1385 let mut cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1386 cfg.commissioner_node_id = 0;
1387 let Err(err) = Commissioner::new(cfg) else {
1390 panic!("zero commissioner_node_id should fail");
1391 };
1392 assert!(
1393 matches!(err, CommissioningError::InvalidConfig(_)),
1394 "got {err:?}"
1395 );
1396 }
1397
1398 #[test]
1399 fn new_rejects_zero_assigned_node_id() {
1400 let fabric = make_fabric_record();
1401 let setup = make_setup_payload();
1402 let paa = PaaTrustStore::with_csa_test_roots();
1403 let cd = CdSigningRoots::with_csa_test_roots();
1404 let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1405 let mut cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1406 cfg.assigned_node_id = 0;
1407 let Err(err) = Commissioner::new(cfg) else {
1408 panic!("zero assigned_node_id should fail");
1409 };
1410 assert!(
1411 matches!(err, CommissioningError::InvalidConfig(_)),
1412 "got {err:?}"
1413 );
1414 }
1415
1416 #[test]
1417 fn new_rejects_equal_commissioner_and_assigned_ids() {
1418 let fabric = make_fabric_record();
1419 let setup = make_setup_payload();
1420 let paa = PaaTrustStore::with_csa_test_roots();
1421 let cd = CdSigningRoots::with_csa_test_roots();
1422 let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1423 let mut cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1424 cfg.commissioner_node_id = 0x42;
1425 cfg.assigned_node_id = 0x42;
1426 let Err(err) = Commissioner::new(cfg) else {
1427 panic!("equal IDs should fail");
1428 };
1429 assert!(
1430 matches!(err, CommissioningError::InvalidConfig(_)),
1431 "got {err:?}"
1432 );
1433 }
1434
1435 #[test]
1436 fn new_rejects_zero_ipk_epoch_key() {
1437 let fabric = make_fabric_record();
1438 let setup = make_setup_payload();
1439 let paa = PaaTrustStore::with_csa_test_roots();
1440 let cd = CdSigningRoots::with_csa_test_roots();
1441 let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1442 let mut cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1443 cfg.ipk_epoch_key = [0u8; 16];
1444 let Err(err) = Commissioner::new(cfg) else {
1445 panic!("zero IPK should fail");
1446 };
1447 assert!(
1448 matches!(err, CommissioningError::InvalidConfig(_)),
1449 "got {err:?}"
1450 );
1451 }
1452
1453 #[test]
1454 fn new_returns_secure_pairing_stage() {
1455 let fabric = make_fabric_record();
1456 let setup = make_setup_payload();
1457 let paa = PaaTrustStore::with_csa_test_roots();
1458 let cd = CdSigningRoots::with_csa_test_roots();
1459 let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1460 let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1461 let sm = Commissioner::new(cfg).expect("valid config should construct");
1462 assert_eq!(sm.stage(), Stage::SecurePairing);
1463 }
1464
1465 #[test]
1466 fn poll_from_secure_pairing_emits_read_commissioning_info() {
1467 let fabric = make_fabric_record();
1468 let setup = make_setup_payload();
1469 let paa = PaaTrustStore::with_csa_test_roots();
1470 let cd = CdSigningRoots::with_csa_test_roots();
1471 let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1472 let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1473 let mut sm = Commissioner::new(cfg).expect("valid config");
1474 let act = sm.poll().expect("poll succeeds");
1475 match act {
1476 Action::ReadAttribute {
1477 session,
1478 endpoint,
1479 cluster,
1480 attributes,
1481 expect,
1482 } => {
1483 assert_eq!(session, crate::state_machine::SessionContext::Pase);
1484 assert_eq!(endpoint, 0);
1485 assert_eq!(cluster, 0x0030);
1486 assert_eq!(expect, Expectation::CommissioningInfo);
1487 assert!(!attributes.is_empty());
1488 }
1489 other => panic!("expected ReadAttribute, got {other:?}"),
1490 }
1491 assert_eq!(sm.stage(), Stage::ReadCommissioningInfo);
1492 }
1493
1494 #[test]
1495 fn poll_is_idempotent_between_responses() {
1496 let fabric = make_fabric_record();
1497 let setup = make_setup_payload();
1498 let paa = PaaTrustStore::with_csa_test_roots();
1499 let cd = CdSigningRoots::with_csa_test_roots();
1500 let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
1501 let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1502 let mut sm = Commissioner::new(cfg).expect("valid config");
1503 let act1 = sm.poll().expect("first poll");
1504 let act2 = sm.poll().expect("second poll");
1505 match (act1, act2) {
1506 (
1507 Action::ReadAttribute {
1508 cluster: c1,
1509 expect: e1,
1510 ..
1511 },
1512 Action::ReadAttribute {
1513 cluster: c2,
1514 expect: e2,
1515 ..
1516 },
1517 ) => {
1518 assert_eq!(c1, c2);
1519 assert_eq!(e1, e2);
1520 }
1521 other => panic!("idempotent poll returned different variants: {other:?}"),
1522 }
1523 }
1524
1525 #[test]
1526 fn full_happy_path_through_config_regulatory_lands_on_send_pai_cert_request() {
1527 let fabric = make_fabric_record();
1528 let setup = make_setup_payload();
1529 let paa = PaaTrustStore::with_csa_test_roots();
1530 let cd = CdSigningRoots::with_csa_test_roots();
1531 let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1532 let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1533 let mut sm = Commissioner::new(cfg).expect("valid config");
1534
1535 let _ = sm.poll().expect("poll #1");
1537 let canned_info = encode_read_commissioning_info_response();
1538 sm.on_response(Expectation::CommissioningInfo, &canned_info)
1539 .expect("commissioning info accepted");
1540 assert_eq!(sm.stage(), Stage::ArmFailsafe);
1541
1542 let _ = sm.poll().expect("poll #2");
1544 sm.on_response(
1545 Expectation::ArmFailsafeResponse,
1546 &[0x15, 0x24, 0x00, 0x00, 0x18],
1547 )
1548 .expect("arm failsafe ok");
1549 assert_eq!(sm.stage(), Stage::ConfigRegulatory);
1550
1551 let _ = sm.poll().expect("poll #3");
1553 sm.on_response(
1554 Expectation::SetRegulatoryConfigResponse,
1555 &[0x15, 0x24, 0x00, 0x00, 0x18],
1556 )
1557 .expect("config regulatory ok");
1558 assert_eq!(sm.stage(), Stage::SendPaiCertRequest);
1559
1560 match sm.poll().expect("poll #4") {
1562 Action::Invoke {
1563 cluster,
1564 command,
1565 expect,
1566 ..
1567 } => {
1568 assert_eq!(cluster, 0x003E);
1569 assert_eq!(command, 0x02);
1570 assert_eq!(expect, Expectation::PaiCertChainResponse);
1571 }
1572 other => panic!("expected Invoke, got {other:?}"),
1573 }
1574 }
1575
1576 #[test]
1577 fn arm_failsafe_busy_response_aborts_with_device_im_status() {
1578 let fabric = make_fabric_record();
1579 let setup = make_setup_payload();
1580 let paa = PaaTrustStore::with_csa_test_roots();
1581 let cd = CdSigningRoots::with_csa_test_roots();
1582 let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1583 let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1584 let mut sm = Commissioner::new(cfg).expect("valid config");
1585 let _ = sm.poll().expect("poll info");
1586 sm.on_response(
1587 Expectation::CommissioningInfo,
1588 &encode_read_commissioning_info_response(),
1589 )
1590 .expect("commissioning info ok");
1591 let _ = sm.poll().expect("poll arm failsafe");
1592 let err = sm
1594 .on_response(
1595 Expectation::ArmFailsafeResponse,
1596 &[0x15, 0x24, 0x00, 0x04, 0x18],
1597 )
1598 .expect_err("busy should fail");
1599 assert!(matches!(
1600 err,
1601 CommissioningError::DeviceImStatus {
1602 stage: Stage::ArmFailsafe,
1603 im_status: 4,
1604 }
1605 ));
1606 assert_eq!(sm.stage(), Stage::Failed);
1607 match sm.poll().expect("abort emission") {
1608 Action::Abort {
1609 send_disarm_failsafe,
1610 reason,
1611 } => {
1612 assert!(send_disarm_failsafe);
1613 assert!(reason.contains("ArmFailsafe"), "reason was {reason}");
1614 assert!(reason.contains("0x4"), "reason was {reason}");
1615 }
1616 other => panic!("expected Abort, got {other:?}"),
1617 }
1618 }
1619
1620 #[test]
1621 fn out_of_order_response_returns_error_without_advancing() {
1622 let fabric = make_fabric_record();
1623 let setup = make_setup_payload();
1624 let paa = PaaTrustStore::with_csa_test_roots();
1625 let cd = CdSigningRoots::with_csa_test_roots();
1626 let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1627 let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1628 let mut sm = Commissioner::new(cfg).expect("valid config");
1629 let err = sm
1631 .on_response(Expectation::ArmFailsafeResponse, &[])
1632 .expect_err("should reject out-of-order");
1633 assert!(matches!(err, CommissioningError::OutOfOrderResponse(_)));
1634 assert_eq!(sm.stage(), Stage::SecurePairing);
1635 }
1636
1637 #[test]
1638 fn wrong_expectation_returns_unexpected_response_kind() {
1639 let fabric = make_fabric_record();
1640 let setup = make_setup_payload();
1641 let paa = PaaTrustStore::with_csa_test_roots();
1642 let cd = CdSigningRoots::with_csa_test_roots();
1643 let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1644 let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1645 let mut sm = Commissioner::new(cfg).expect("valid config");
1646 let _ = sm.poll().expect("poll");
1647 let err = sm
1648 .on_response(Expectation::ArmFailsafeResponse, &[])
1649 .expect_err("wrong kind should fail");
1650 assert!(matches!(
1651 err,
1652 CommissioningError::UnexpectedResponseKind {
1653 expected: Expectation::CommissioningInfo,
1654 got: Expectation::ArmFailsafeResponse,
1655 }
1656 ));
1657 assert_eq!(sm.stage(), Stage::ReadCommissioningInfo);
1659 }
1660
1661 fn encode_read_commissioning_info_response() -> Vec<u8> {
1662 vec![0x15, 0x18]
1665 }
1666
1667 fn drive_to_send_pai_cert_request(sm: &mut Commissioner) {
1670 let _ = sm.poll().expect("poll info");
1671 sm.on_response(Expectation::CommissioningInfo, &[0x15, 0x18])
1672 .expect("info ok");
1673 let _ = sm.poll().expect("poll arm failsafe");
1674 sm.on_response(
1675 Expectation::ArmFailsafeResponse,
1676 &[0x15, 0x24, 0x00, 0x00, 0x18],
1677 )
1678 .expect("arm ok");
1679 let _ = sm.poll().expect("poll config regulatory");
1680 sm.on_response(
1681 Expectation::SetRegulatoryConfigResponse,
1682 &[0x15, 0x24, 0x00, 0x00, 0x18],
1683 )
1684 .expect("regulatory ok");
1685 }
1686
1687 fn synthetic_cert_chain_response(cert: &[u8]) -> Vec<u8> {
1688 use matter_codec::{Tag, TlvWriter};
1689 let mut buf = Vec::new();
1690 let mut w = TlvWriter::new(&mut buf);
1691 w.start_structure(Tag::Anonymous).expect("infallible");
1692 w.put_bytes(Tag::Context(0), cert).expect("infallible");
1693 w.end_container().expect("infallible");
1694 buf
1695 }
1696
1697 fn nonce_from_attestation_invoke(act: &Action) -> [u8; 32] {
1698 match act {
1699 Action::Invoke { payload, .. } => {
1700 use matter_codec::{Element, Tag, TlvReader, Value};
1701 let mut r = TlvReader::new(payload);
1702 let _ = r.next().expect("reader").expect("anon-struct-start");
1703 loop {
1704 match r.next().expect("reader") {
1705 Some(Element::Scalar {
1706 tag: Tag::Context(0),
1707 value: Value::Bytes(b),
1708 }) => {
1709 return b.as_slice().try_into().expect("32 bytes");
1710 }
1711 Some(_) => {}
1712 None => panic!("no nonce found"),
1713 }
1714 }
1715 }
1716 other => panic!("expected Invoke, got {other:?}"),
1717 }
1718 }
1719
1720 #[test]
1721 fn poll_at_send_pai_emits_certificate_chain_request_pai() {
1722 let fabric = make_fabric_record();
1723 let setup = make_setup_payload();
1724 let paa = PaaTrustStore::with_csa_test_roots();
1725 let cd = CdSigningRoots::with_csa_test_roots();
1726 let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1727 let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1728 let mut sm = Commissioner::new(cfg).expect("valid config");
1729 drive_to_send_pai_cert_request(&mut sm);
1730 assert_eq!(sm.stage(), Stage::SendPaiCertRequest);
1731 match sm.poll().expect("poll PAI") {
1732 Action::Invoke {
1733 cluster,
1734 command,
1735 expect,
1736 payload,
1737 ..
1738 } => {
1739 assert_eq!(cluster, 0x003E);
1740 assert_eq!(command, 0x02);
1741 assert_eq!(expect, Expectation::PaiCertChainResponse);
1742 assert_eq!(payload, vec![0x15, 0x24, 0x00, 0x02, 0x18]);
1744 }
1745 other => panic!("expected Invoke, got {other:?}"),
1746 }
1747 }
1748
1749 #[test]
1750 fn poll_at_send_dac_emits_certificate_chain_request_dac() {
1751 let fabric = make_fabric_record();
1752 let setup = make_setup_payload();
1753 let paa = PaaTrustStore::with_csa_test_roots();
1754 let cd = CdSigningRoots::with_csa_test_roots();
1755 let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1756 let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1757 let mut sm = Commissioner::new(cfg).expect("valid config");
1758 drive_to_send_pai_cert_request(&mut sm);
1759 let _ = sm.poll().expect("poll PAI");
1760 let pai_response = synthetic_cert_chain_response(&[0xAA, 0xBB, 0xCC]);
1761 sm.on_response(Expectation::PaiCertChainResponse, &pai_response)
1762 .expect("PAI accepted");
1763 assert_eq!(sm.stage(), Stage::SendDacCertRequest);
1764 match sm.poll().expect("poll DAC") {
1765 Action::Invoke {
1766 cluster,
1767 command,
1768 expect,
1769 payload,
1770 ..
1771 } => {
1772 assert_eq!(cluster, 0x003E);
1773 assert_eq!(command, 0x02);
1774 assert_eq!(expect, Expectation::DacCertChainResponse);
1775 assert_eq!(payload, vec![0x15, 0x24, 0x00, 0x01, 0x18]);
1777 }
1778 other => panic!("expected Invoke, got {other:?}"),
1779 }
1780 }
1781
1782 #[test]
1783 fn send_attestation_request_uses_fresh_random_nonce_each_time() {
1784 let fabric = make_fabric_record();
1785 let setup = make_setup_payload();
1786 let paa = PaaTrustStore::with_csa_test_roots();
1787 let cd = CdSigningRoots::with_csa_test_roots();
1788
1789 let rng_a: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1790 let cfg_a = base_config(&fabric, &setup, &paa, &cd, rng_a);
1791 let mut sm_a = Commissioner::new(cfg_a).expect("valid config");
1792 drive_to_send_pai_cert_request(&mut sm_a);
1793 let _ = sm_a.poll().expect("poll PAI a");
1794 let pai_response = synthetic_cert_chain_response(&[0xAA]);
1795 sm_a.on_response(Expectation::PaiCertChainResponse, &pai_response)
1796 .expect("ok");
1797 let _ = sm_a.poll().expect("poll DAC a");
1798 let dac_response = synthetic_cert_chain_response(&[0xBB]);
1799 sm_a.on_response(Expectation::DacCertChainResponse, &dac_response)
1800 .expect("ok");
1801 let nonce_a = nonce_from_attestation_invoke(&sm_a.poll().expect("poll att a"));
1802
1803 let rng_b: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1804 let cfg_b = base_config(&fabric, &setup, &paa, &cd, rng_b);
1805 let mut sm_b = Commissioner::new(cfg_b).expect("valid config");
1806 drive_to_send_pai_cert_request(&mut sm_b);
1807 let _ = sm_b.poll().expect("poll PAI b");
1808 sm_b.on_response(Expectation::PaiCertChainResponse, &pai_response)
1809 .expect("ok");
1810 let _ = sm_b.poll().expect("poll DAC b");
1811 sm_b.on_response(Expectation::DacCertChainResponse, &dac_response)
1812 .expect("ok");
1813 let nonce_b = nonce_from_attestation_invoke(&sm_b.poll().expect("poll att b"));
1814
1815 assert_ne!(
1816 nonce_a, nonce_b,
1817 "two independent runs should use different random nonces"
1818 );
1819 }
1820
1821 fn nonce_from_csr_invoke(act: &Action) -> [u8; 32] {
1827 match act {
1828 Action::Invoke { payload, .. } => {
1829 use matter_codec::{Element, Tag, TlvReader, Value};
1830 let mut r = TlvReader::new(payload);
1831 let _ = r.next().expect("reader").expect("anon-struct-start");
1832 loop {
1833 match r.next().expect("reader") {
1834 Some(Element::Scalar {
1835 tag: Tag::Context(0),
1836 value: Value::Bytes(b),
1837 }) => {
1838 return b.as_slice().try_into().expect("32 bytes");
1839 }
1840 Some(_) => {}
1841 None => panic!("no nonce found"),
1842 }
1843 }
1844 }
1845 other => panic!("expected Invoke, got {other:?}"),
1846 }
1847 }
1848
1849 #[test]
1856 fn send_op_cert_signing_request_emits_csr_with_random_nonce() {
1857 let fabric = make_fabric_record();
1858 let setup = make_setup_payload();
1859 let paa = PaaTrustStore::with_csa_test_roots();
1860 let cd = CdSigningRoots::with_csa_test_roots();
1861
1862 let rng_a: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1863 let cfg_a = base_config(&fabric, &setup, &paa, &cd, rng_a);
1864 let mut sm_a = Commissioner::new(cfg_a).expect("valid config");
1865 sm_a.stage = Stage::SendOpCertSigningRequest;
1869 sm_a.dac_der = Some(vec![0xAA, 0xBB]);
1870 let act_a = sm_a.poll().expect("poll csr a");
1871 let nonce_a = nonce_from_csr_invoke(&act_a);
1872
1873 let rng_b: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1874 let cfg_b = base_config(&fabric, &setup, &paa, &cd, rng_b);
1875 let mut sm_b = Commissioner::new(cfg_b).expect("valid config");
1876 sm_b.stage = Stage::SendOpCertSigningRequest;
1877 sm_b.dac_der = Some(vec![0xAA, 0xBB]);
1878 let act_b = sm_b.poll().expect("poll csr b");
1879 let nonce_b = nonce_from_csr_invoke(&act_b);
1880
1881 assert_ne!(
1882 nonce_a, nonce_b,
1883 "two independent runs should use different CSR nonces"
1884 );
1885
1886 match act_a {
1887 Action::Invoke {
1888 cluster,
1889 command,
1890 expect,
1891 ..
1892 } => {
1893 assert_eq!(cluster, 0x003E);
1894 assert_eq!(command, 0x04);
1895 assert_eq!(expect, Expectation::CsrResponse);
1896 }
1897 other => panic!("expected Invoke, got {other:?}"),
1898 }
1899 }
1900
1901 #[test]
1908 fn drive_through_send_noc_with_synthetic_noc_response() {
1909 use matter_cert::{
1910 BasicConstraints, DistinguishedName, DnAttribute, Extensions, MatterCertificate,
1911 PublicKey,
1912 };
1913
1914 let fabric = make_fabric_record();
1915 let setup = make_setup_payload();
1916 let paa = PaaTrustStore::with_csa_test_roots();
1917 let cd = CdSigningRoots::with_csa_test_roots();
1918 let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1919 let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1920 let mut sm = Commissioner::new(cfg).expect("valid config");
1921
1922 let mut key_bytes = [0u8; 65];
1928 key_bytes[0] = 0x04;
1929 let synthetic_noc = MatterCertificate::builder()
1930 .serial(vec![1, 2, 3])
1931 .issuer(fabric.root_cert.subject().clone())
1932 .subject(DistinguishedName::new(vec![
1933 DnAttribute::FabricId(fabric.fabric_id),
1934 DnAttribute::NodeId(0x2),
1935 ]))
1936 .validity(
1937 MatterTime::from_unix_secs(1_704_067_200),
1938 MatterTime::NO_EXPIRY,
1939 )
1940 .public_key(PublicKey::new(key_bytes).expect("valid sec1 prefix"))
1941 .extensions(
1942 Extensions::builder()
1943 .basic_constraints(Some(BasicConstraints::new(false, None)))
1944 .build(),
1945 )
1946 .build_unsigned()
1947 .expect("builder")
1948 .assemble([0u8; 64]);
1949
1950 sm.stage = Stage::SendNoc;
1951 sm.issued_noc = Some(synthetic_noc);
1952 sm.issued_noc_public_key = Some(key_bytes);
1953
1954 match sm.poll().expect("poll SendNoc") {
1955 Action::Invoke {
1956 cluster,
1957 command,
1958 expect,
1959 ..
1960 } => {
1961 assert_eq!(cluster, 0x003E);
1962 assert_eq!(command, 0x06);
1963 assert_eq!(expect, Expectation::NocResponse);
1964 }
1965 other => panic!("expected Invoke, got {other:?}"),
1966 }
1967
1968 let mut noc_response = Vec::new();
1970 {
1971 use matter_codec::{Tag, TlvWriter};
1972 let mut w = TlvWriter::new(&mut noc_response);
1973 w.start_structure(Tag::Anonymous).expect("infallible");
1974 w.put_uint(Tag::Context(0), 0).expect("infallible"); w.put_uint(Tag::Context(1), 1).expect("infallible"); w.end_container().expect("infallible");
1977 }
1978 sm.on_response(Expectation::NocResponse, &noc_response)
1979 .expect("NocResponse accepted");
1980 assert_eq!(sm.stage(), Stage::ReadNetworkCommissioningInfo);
1981 }
1982
1983 #[test]
1987 fn send_noc_failure_status_aborts_with_device_im_status() {
1988 use matter_cert::{
1989 BasicConstraints, DistinguishedName, DnAttribute, Extensions, MatterCertificate,
1990 PublicKey,
1991 };
1992
1993 let fabric = make_fabric_record();
1994 let setup = make_setup_payload();
1995 let paa = PaaTrustStore::with_csa_test_roots();
1996 let cd = CdSigningRoots::with_csa_test_roots();
1997 let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
1998 let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
1999 let mut sm = Commissioner::new(cfg).expect("valid config");
2000
2001 let mut key_bytes = [0u8; 65];
2002 key_bytes[0] = 0x04;
2003 let synthetic_noc = MatterCertificate::builder()
2004 .serial(vec![9])
2005 .issuer(fabric.root_cert.subject().clone())
2006 .subject(DistinguishedName::new(vec![
2007 DnAttribute::FabricId(fabric.fabric_id),
2008 DnAttribute::NodeId(0x2),
2009 ]))
2010 .validity(
2011 MatterTime::from_unix_secs(1_704_067_200),
2012 MatterTime::NO_EXPIRY,
2013 )
2014 .public_key(PublicKey::new(key_bytes).expect("valid sec1 prefix"))
2015 .extensions(
2016 Extensions::builder()
2017 .basic_constraints(Some(BasicConstraints::new(false, None)))
2018 .build(),
2019 )
2020 .build_unsigned()
2021 .expect("builder")
2022 .assemble([0u8; 64]);
2023
2024 sm.stage = Stage::SendNoc;
2025 sm.issued_noc = Some(synthetic_noc);
2026 sm.issued_noc_public_key = Some(key_bytes);
2027
2028 let _ = sm.poll().expect("poll SendNoc");
2029
2030 let mut bad_response = Vec::new();
2032 {
2033 use matter_codec::{Tag, TlvWriter};
2034 let mut w = TlvWriter::new(&mut bad_response);
2035 w.start_structure(Tag::Anonymous).expect("infallible");
2036 w.put_uint(Tag::Context(0), 9).expect("infallible");
2037 w.end_container().expect("infallible");
2038 }
2039 let err = sm
2040 .on_response(Expectation::NocResponse, &bad_response)
2041 .expect_err("non-zero NOC status should fail");
2042 assert!(matches!(
2043 err,
2044 CommissioningError::DeviceImStatus {
2045 stage: Stage::SendNoc,
2046 im_status: 9,
2047 }
2048 ));
2049 assert_eq!(sm.stage(), Stage::Failed);
2050 }
2051
2052 #[test]
2057 fn send_trusted_root_cert_emits_invoke_and_status_ack_advances() {
2058 let fabric = make_fabric_record();
2059 let setup = make_setup_payload();
2060 let paa = PaaTrustStore::with_csa_test_roots();
2061 let cd = CdSigningRoots::with_csa_test_roots();
2062 let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2063 let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2064 let mut sm = Commissioner::new(cfg).expect("valid config");
2065
2066 sm.stage = Stage::SendTrustedRootCert;
2067
2068 match sm.poll().expect("poll SendTrustedRootCert") {
2069 Action::Invoke {
2070 cluster,
2071 command,
2072 expect,
2073 payload,
2074 ..
2075 } => {
2076 assert_eq!(cluster, 0x003E);
2077 assert_eq!(command, 0x0B);
2078 assert_eq!(expect, Expectation::AddTrustedRootResponse);
2079 assert!(payload.len() > 16, "RCAC TLV too short: {}", payload.len());
2082 assert_eq!(payload[0], 0x15); }
2084 other => panic!("expected Invoke, got {other:?}"),
2085 }
2086
2087 sm.on_response(Expectation::AddTrustedRootResponse, &[0x00])
2089 .expect("status-ack accepted");
2090 assert_eq!(sm.stage(), Stage::SendNoc);
2091 }
2092
2093 #[test]
2096 fn find_operational_for_complete_emits_establish_case() {
2097 let fabric = make_fabric_record();
2098 let setup = make_setup_payload();
2099 let paa = PaaTrustStore::with_csa_test_roots();
2100 let cd = crate::attestation::CdSigningRoots::with_csa_test_roots();
2101 let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2102 let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2103 let mut sm = Commissioner::new(cfg).expect("valid config");
2104 sm.stage = Stage::FindOperationalForComplete;
2105 match sm.poll().expect("poll establish case") {
2106 Action::EstablishCase {
2107 fabric_id,
2108 peer_node_id,
2109 } => {
2110 assert_eq!(fabric_id, fabric.fabric_id);
2111 assert_eq!(peer_node_id, 0x2); }
2113 other => panic!("expected EstablishCase, got {other:?}"),
2114 }
2115 assert!(sm.awaiting_case_session);
2116 }
2117
2118 #[test]
2119 fn on_case_established_advances_to_send_complete() {
2120 let fabric = make_fabric_record();
2121 let setup = make_setup_payload();
2122 let paa = PaaTrustStore::with_csa_test_roots();
2123 let cd = crate::attestation::CdSigningRoots::with_csa_test_roots();
2124 let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2125 let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2126 let mut sm = Commissioner::new(cfg).expect("valid config");
2127 sm.stage = Stage::FindOperationalForComplete;
2128 let _ = sm.poll().expect("emit EstablishCase");
2129 sm.on_case_established().expect("case established");
2130 assert_eq!(sm.stage(), Stage::SendComplete);
2131 assert!(!sm.awaiting_case_session);
2132 }
2133
2134 #[test]
2135 fn on_case_established_without_pending_emits_out_of_order() {
2136 let fabric = make_fabric_record();
2137 let setup = make_setup_payload();
2138 let paa = PaaTrustStore::with_csa_test_roots();
2139 let cd = crate::attestation::CdSigningRoots::with_csa_test_roots();
2140 let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2141 let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2142 let mut sm = Commissioner::new(cfg).expect("valid config");
2143 let err = sm.on_case_established().expect_err("no pending establish");
2144 assert!(matches!(err, CommissioningError::OutOfOrderResponse(_)));
2145 }
2146
2147 #[test]
2148 fn send_complete_emits_invoke_over_case_session() {
2149 let fabric = make_fabric_record();
2150 let setup = make_setup_payload();
2151 let paa = PaaTrustStore::with_csa_test_roots();
2152 let cd = crate::attestation::CdSigningRoots::with_csa_test_roots();
2153 let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2154 let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2155 let mut sm = Commissioner::new(cfg).expect("valid config");
2156 sm.stage = Stage::SendComplete;
2157 match sm.poll().expect("poll send complete") {
2158 Action::Invoke {
2159 session,
2160 cluster,
2161 command,
2162 expect,
2163 payload,
2164 ..
2165 } => {
2166 assert_eq!(session, crate::state_machine::SessionContext::Case);
2167 assert_eq!(cluster, 0x0030);
2168 assert_eq!(command, 0x04); assert_eq!(expect, Expectation::CommissioningCompleteResponse);
2170 assert_eq!(payload, vec![0x15, 0x18]);
2172 }
2173 other => panic!("expected Invoke, got {other:?}"),
2174 }
2175 }
2176
2177 #[test]
2178 fn send_complete_success_advances_to_cleanup() {
2179 let fabric = make_fabric_record();
2180 let setup = make_setup_payload();
2181 let paa = PaaTrustStore::with_csa_test_roots();
2182 let cd = crate::attestation::CdSigningRoots::with_csa_test_roots();
2183 let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2184 let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2185 let mut sm = Commissioner::new(cfg).expect("valid config");
2186 sm.stage = Stage::SendComplete;
2187 let _ = sm.poll().expect("emit invoke");
2188 sm.on_response(
2189 Expectation::CommissioningCompleteResponse,
2190 &[0x15, 0x24, 0x00, 0x00, 0x18], )
2192 .expect("complete ok");
2193 assert_eq!(sm.stage(), Stage::Cleanup);
2194 }
2195
2196 #[test]
2197 fn cleanup_emits_done_with_noc_public_key() {
2198 let fabric = make_fabric_record();
2199 let setup = make_setup_payload();
2200 let paa = PaaTrustStore::with_csa_test_roots();
2201 let cd = crate::attestation::CdSigningRoots::with_csa_test_roots();
2202 let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2203 let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2204 let mut sm = Commissioner::new(cfg).expect("valid config");
2205 sm.stage = Stage::Cleanup;
2206 sm.issued_noc_public_key = Some([0xCA; 65]);
2207 match sm.poll().expect("poll cleanup") {
2208 Action::Done(cf) => {
2209 assert_eq!(cf.peer_node_id, 0x2);
2210 assert_eq!(cf.peer_root_public_key, [0xCA; 65]);
2211 assert_eq!(cf.terminated_at, Stage::Cleanup);
2212 assert_eq!(cf.fabric.fabric_id, fabric.fabric_id);
2213 }
2214 other => panic!("expected Done, got {other:?}"),
2215 }
2216 }
2217
2218 #[test]
2221 fn case_failed_response_aborts_with_case_establishment_failed() {
2222 let fabric = make_fabric_record();
2223 let setup = make_setup_payload();
2224 let paa = PaaTrustStore::with_csa_test_roots();
2225 let cd = crate::attestation::CdSigningRoots::with_csa_test_roots();
2226 let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2227 let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2228 let mut sm = Commissioner::new(cfg).expect("valid config");
2229
2230 sm.stage = Stage::FindOperationalForComplete;
2233 let _ = sm.poll().expect("emit EstablishCase");
2234 assert!(sm.awaiting_case_session);
2235
2236 let err = sm
2238 .on_response(Expectation::CaseFailed, &[])
2239 .expect_err("CaseFailed should error");
2240 assert!(matches!(err, CommissioningError::CaseEstablishmentFailed));
2241 assert_eq!(sm.stage(), Stage::Failed);
2242 assert!(!sm.awaiting_case_session);
2243
2244 match sm.poll().expect("emit abort") {
2246 Action::Abort {
2247 send_disarm_failsafe,
2248 reason,
2249 } => {
2250 assert!(send_disarm_failsafe);
2251 assert!(
2252 reason.contains("CASE"),
2253 "abort reason should mention CASE: {reason}"
2254 );
2255 }
2256 other => panic!("expected Abort, got {other:?}"),
2257 }
2258 }
2259
2260 #[test]
2261 fn case_failed_when_not_awaiting_returns_out_of_order() {
2262 let fabric = make_fabric_record();
2263 let setup = make_setup_payload();
2264 let paa = PaaTrustStore::with_csa_test_roots();
2265 let cd = crate::attestation::CdSigningRoots::with_csa_test_roots();
2266 let rng: Arc<dyn crate::noc::NocRng> = Arc::new(SystemNocRng);
2267 let cfg = base_config(&fabric, &setup, &paa, &cd, rng);
2268 let mut sm = Commissioner::new(cfg).expect("valid config");
2269 let err = sm
2270 .on_response(Expectation::CaseFailed, &[])
2271 .expect_err("CaseFailed without pending should error");
2272 assert!(matches!(err, CommissioningError::OutOfOrderResponse(_)));
2273 }
2274
2275 fn sample_valid_config() -> CommissionerConfig<'static> {
2279 use std::sync::OnceLock;
2280 static FABRIC: OnceLock<FabricRecord> = OnceLock::new();
2281 static SETUP: OnceLock<SetupPayload> = OnceLock::new();
2282 static PAA: OnceLock<PaaTrustStore> = OnceLock::new();
2283 static CD: OnceLock<crate::attestation::CdSigningRoots> = OnceLock::new();
2284
2285 let fabric = FABRIC.get_or_init(make_fabric_record);
2286 let setup = SETUP.get_or_init(make_setup_payload);
2287 let paa = PAA.get_or_init(PaaTrustStore::with_csa_test_roots);
2288 let cd = CD.get_or_init(crate::attestation::CdSigningRoots::with_csa_test_roots);
2289 let rng: Arc<dyn NocRng> = Arc::new(SystemNocRng);
2290
2291 CommissionerConfig {
2292 pase_attestation_challenge: [0u8; 16],
2293 fabric,
2294 setup_payload: setup,
2295 paa_trust_store: paa,
2296 cd_signing_roots: cd,
2297 commissioner_node_id: 0x1,
2298 assigned_node_id: 0x2,
2299 ipk_epoch_key: [0x42_u8; 16],
2300 case_admin_subject: 0x1,
2301 admin_vendor_id: 0xFFF1,
2302 now: MatterTime::from_unix_secs(1_704_067_200),
2303 rng,
2304 network: NetworkCredentials::AlreadyOnNetwork,
2305 }
2306 }
2307
2308 #[test]
2309 fn empty_ssid_is_rejected() {
2310 let mut config = sample_valid_config();
2311 config.network = NetworkCredentials::WiFi(WiFiCredentials {
2312 ssid: vec![],
2313 credentials: vec![],
2314 });
2315 let Err(err) = Commissioner::new(config) else {
2316 panic!("empty ssid should fail");
2317 };
2318 assert!(
2319 matches!(err, CommissioningError::InvalidConfig(m) if m.contains("ssid")),
2320 "got {err:?}",
2321 );
2322 }
2323
2324 #[test]
2325 fn oversize_ssid_is_rejected() {
2326 let mut config = sample_valid_config();
2327 config.network = NetworkCredentials::WiFi(WiFiCredentials {
2328 ssid: vec![b'a'; 33],
2329 credentials: vec![],
2330 });
2331 let Err(err) = Commissioner::new(config) else {
2332 panic!("33-byte ssid should fail");
2333 };
2334 assert!(
2335 matches!(err, CommissioningError::InvalidConfig(m) if m.contains("≤32")),
2336 "got {err:?}",
2337 );
2338 }
2339
2340 #[test]
2341 fn oversize_credentials_is_rejected() {
2342 let mut config = sample_valid_config();
2343 config.network = NetworkCredentials::WiFi(WiFiCredentials {
2344 ssid: b"matter".to_vec(),
2345 credentials: vec![0u8; 65],
2346 });
2347 let Err(err) = Commissioner::new(config) else {
2348 panic!("65-byte credentials should fail");
2349 };
2350 assert!(
2351 matches!(err, CommissioningError::InvalidConfig(m) if m.contains("≤64")),
2352 "got {err:?}",
2353 );
2354 }
2355
2356 #[test]
2357 fn wifi_credentials_none_is_accepted() {
2358 let mut config = sample_valid_config();
2359 config.network = NetworkCredentials::AlreadyOnNetwork;
2360 Commissioner::new(config).expect("AlreadyOnNetwork should pass validation");
2361 }
2362
2363 #[test]
2364 fn network_credentials_thread_variant_accepted() {
2365 let ds =
2368 crate::thread_dataset::ThreadDataset::new(vec![0x02, 0x08, 0, 0, 0, 0, 0, 0, 0, 0])
2369 .expect("minimal ext-pan-id dataset is valid");
2370 let mut config = sample_valid_config();
2371 config.network = NetworkCredentials::Thread(ds);
2372 let c = Commissioner::new(config).expect("Thread network should pass validation");
2373 assert!(matches!(c.network(), NetworkCredentials::Thread(_)));
2374 }
2375
2376 #[test]
2377 fn wifi_credentials_debug_redacts_passphrase() {
2378 let creds = WiFiCredentials {
2379 ssid: b"matter".to_vec(),
2380 credentials: b"hunter22".to_vec(),
2381 };
2382 let rendered = format!("{creds:?}");
2383 assert!(
2384 !rendered.contains("hunter22"),
2385 "Debug must not contain credentials bytes: {rendered}",
2386 );
2387 assert!(rendered.contains("redacted"), "got {rendered}");
2388 assert!(
2389 rendered.contains('8'),
2390 "credentials length should appear: {rendered}"
2391 );
2392 assert!(
2393 rendered.contains('6'),
2394 "ssid length should appear: {rendered}"
2395 );
2396 }
2397
2398 #[test]
2401 fn failsafe_expiry_derives_from_basic_commissioning_info() {
2402 let mut sm = Commissioner::new(sample_valid_config()).expect("valid config");
2403 let _initial = sm.poll().expect("initial poll");
2405 let response = vec![
2406 0x15, 0x25, 0x00, 0x78, 0x00, 0x18,
2408 ];
2409 sm.on_response(Expectation::CommissioningInfo, &response)
2410 .expect("commissioning info accepted");
2411 let action = sm.poll().expect("arm-failsafe poll");
2413 match action {
2414 Action::Invoke {
2415 payload,
2416 cluster,
2417 command,
2418 ..
2419 } => {
2420 assert_eq!(cluster, 0x0030);
2421 assert_eq!(command, 0x00);
2422 assert!(
2425 payload.windows(3).any(|w| w == [0x24, 0x00, 0x78]),
2426 "ArmFailSafe payload should carry expiry=120: {payload:02x?}",
2427 );
2428 }
2429 other => panic!("expected Invoke, got {other:?}"),
2430 }
2431 }
2432
2433 #[test]
2434 fn failsafe_expiry_falls_back_to_60_on_empty_basic_commissioning_info() {
2435 let mut sm = Commissioner::new(sample_valid_config()).expect("valid config");
2436 let _initial = sm.poll().expect("initial poll");
2437 sm.on_response(Expectation::CommissioningInfo, &[0x15, 0x18])
2441 .expect("empty struct accepted");
2442 let action = sm.poll().expect("arm-failsafe poll");
2443 if let Action::Invoke { payload, .. } = action {
2444 assert!(
2446 payload.windows(3).any(|w| w == [0x24, 0x00, 0x3C]),
2447 "ArmFailSafe payload should carry expiry=60 fallback: {payload:02x?}",
2448 );
2449 }
2450 }
2451
2452 #[test]
2455 fn breadcrumb_increases_across_commands() {
2456 fn extract_uint_at_tag(payload: &[u8], tag_num: u8) -> Option<u64> {
2457 use matter_codec::{Element, Tag, TlvReader, Value};
2458 let mut reader = TlvReader::new(payload);
2459 while let Ok(Some(elem)) = reader.next() {
2460 if let Element::Scalar {
2461 tag: Tag::Context(t),
2462 value: Value::Uint(v),
2463 } = elem
2464 {
2465 if t == tag_num {
2466 return Some(v);
2467 }
2468 }
2469 }
2470 None
2471 }
2472
2473 let mut sm = Commissioner::new(sample_valid_config()).expect("valid config");
2474 let mut breadcrumbs: Vec<u64> = Vec::new();
2475
2476 let _ = sm.poll().expect("read commissioning info");
2478 sm.on_response(Expectation::CommissioningInfo, &[0x15, 0x18])
2479 .expect("info accepted");
2480
2481 let action = sm.poll().expect("arm-failsafe");
2483 if let Action::Invoke { payload, .. } = action {
2484 if let Some(b) = extract_uint_at_tag(&payload, 1) {
2485 breadcrumbs.push(b);
2486 }
2487 }
2488 sm.on_response(
2489 Expectation::ArmFailsafeResponse,
2490 &[0x15, 0x24, 0x00, 0x00, 0x18],
2491 )
2492 .expect("arm-failsafe ok");
2493
2494 let action = sm.poll().expect("set-regulatory");
2496 if let Action::Invoke { payload, .. } = action {
2497 if let Some(b) = extract_uint_at_tag(&payload, 2) {
2498 breadcrumbs.push(b);
2499 }
2500 }
2501
2502 assert_eq!(
2503 breadcrumbs.len(),
2504 2,
2505 "should have extracted exactly two breadcrumbs, got {breadcrumbs:?}",
2506 );
2507 assert!(
2508 breadcrumbs[0] < breadcrumbs[1],
2509 "breadcrumbs should be strictly increasing: {breadcrumbs:?}",
2510 );
2511 }
2512}