1use core::num::NonZeroU8;
19
20use embassy_time::{Duration, Instant};
21
22use crate::cert::{CertRef, MAX_CERT_TLV_LEN};
23use crate::crypto::{
24 CanonAeadKeyRef, CanonPkcSecretKey, CanonPkcSecretKeyRef, Crypto, PublicKey, SecretKey,
25 SigningSecretKey, PKC_SECRET_KEY_ZEROED,
26};
27use crate::dm::clusters::net_comm::NetworksAccess;
28use crate::dm::clusters::time_sync::UtcTime;
29use crate::dm::endpoints::ROOT_ENDPOINT_ID;
30use crate::dm::{ClusterId, EndptId};
31use crate::error::{Error, ErrorCode};
32use crate::fabric::{Fabric, Fabrics};
33use crate::im::IMStatusCode;
34use crate::persist::{KvBlobStoreAccess, NETWORKS_KEY};
35use crate::sc::pase::Pase;
36use crate::tlv::TLVElement;
37use crate::transport::session::SessionMode;
38use crate::utils::bitflags::bitflags;
39use crate::utils::init::{init, Init};
40use crate::utils::storage::Vec;
41
42bitflags! {
43 #[repr(transparent)]
44 #[derive(Default)]
45 #[cfg_attr(not(feature = "defmt"), derive(Debug, Copy, Clone, Eq, PartialEq, Hash))]
46 pub struct NocFlags: u8 {
47 const ADD_CSR_REQ_RECVD = 0x01;
48 const UPDATE_CSR_REQ_RECVD = 0x02;
49 const ADD_ROOT_CERT_RECVD = 0x04;
50 const ADD_NOC_RECVD = 0x08;
51 const UPDATE_NOC_RECVD = 0x10;
52 }
53}
54
55#[derive(PartialEq)]
56pub struct ArmedCtx {
57 armed_at: Instant,
58 timeout_secs: u16,
59 fab_idx: u8,
60 flags: NocFlags,
61}
62
63#[derive(PartialEq)]
64pub enum State {
65 Idle,
66 Armed(ArmedCtx),
67}
68
69pub enum IMError {
70 Error(Error),
71 Status(IMStatusCode),
72}
73
74impl From<Error> for IMError {
75 fn from(e: Error) -> Self {
76 IMError::Error(e)
77 }
78}
79
80impl From<IMStatusCode> for IMError {
81 fn from(e: IMStatusCode) -> Self {
82 IMError::Status(e)
83 }
84}
85
86pub const DEFAULT_FAILSAFE_EXPIRY_SECS: u16 = 60;
90
91pub struct FailSafe {
92 state: State,
93 secret_key: CanonPkcSecretKey,
94 root_ca: Vec<u8, { MAX_CERT_TLV_LEN }>,
95 breadcrumb: u64,
96}
97
98impl FailSafe {
99 #[inline(always)]
100 pub const fn new() -> Self {
101 Self {
102 state: State::Idle,
103 secret_key: PKC_SECRET_KEY_ZEROED,
104 root_ca: Vec::new(),
105 breadcrumb: 0,
106 }
107 }
108
109 pub fn init() -> impl Init<Self> {
110 init!(Self {
111 state: State::Idle,
112 secret_key <- CanonPkcSecretKey::init(),
113 root_ca <- Vec::init(),
114 breadcrumb: 0
115 })
116 }
117
118 #[allow(clippy::too_many_arguments)]
129 pub fn check_failsafe_timeout<S, N>(
130 &mut self,
131 fabrics: &mut Fabrics,
132 sessions: &mut crate::transport::session::Sessions,
133 networks: N,
134 kv: S,
135 expire_sess_id: Option<u32>,
136 mdns_notif: impl FnMut(),
137 notify_change: impl FnMut(EndptId, ClusterId),
138 ) -> Result<Option<NonZeroU8>, Error>
139 where
140 S: KvBlobStoreAccess,
141 N: NetworksAccess,
142 {
143 if let State::Armed(ctx) = &self.state {
144 let now = Instant::now();
145 if now
146 >= ctx
147 .armed_at
148 .saturating_add(Duration::from_secs(ctx.timeout_secs as u64))
149 {
150 return self.expire(
154 fabrics,
155 sessions,
156 expire_sess_id,
157 networks,
158 kv,
159 mdns_notif,
160 notify_change,
161 );
162 }
163 }
164
165 Ok(None)
166 }
167
168 #[allow(clippy::too_many_arguments)]
186 pub fn expire<S, N>(
187 &mut self,
188 fabrics: &mut Fabrics,
189 sessions: &mut crate::transport::session::Sessions,
190 expire_sess_id: Option<u32>,
191 networks: N,
192 kv: S,
193 mut mdns_notif: impl FnMut(),
194 mut notify_change: impl FnMut(EndptId, ClusterId),
195 ) -> Result<Option<NonZeroU8>, Error>
196 where
197 S: KvBlobStoreAccess,
198 N: NetworksAccess,
199 {
200 let State::Armed(ctx) = &self.state else {
201 return Ok(None);
202 };
203
204 warn!(
205 "Fail-Safe timeout expired for fabric {}, disarming",
206 ctx.fab_idx
207 );
208
209 let fab_idx_raw = ctx.fab_idx;
210 let mut removed_fabric = None;
211
212 kv.access(|mut kv, buf| {
213 if let Some(fab_idx) = NonZeroU8::new(fab_idx_raw) {
214 fabrics.remove(fab_idx)?;
215 fabrics.add_load(fab_idx.get(), &mut kv, buf)?;
216
217 removed_fabric = fabrics.get(fab_idx).is_none().then_some(fab_idx);
218 }
219
220 networks.access(|networks| {
221 let data = kv.load(NETWORKS_KEY, buf)?;
222
223 if let Some(data) = data {
224 networks.load(data)
225 } else {
226 networks.reset()
227 }
228 })
229 })?;
230
231 sessions.remove_pase(expire_sess_id);
239
240 self.state = State::Idle;
241 self.breadcrumb = 0;
242
243 mdns_notif();
244
245 notify_change(
257 ROOT_ENDPOINT_ID,
258 crate::dm::clusters::decl::operational_credentials::FULL_CLUSTER.id,
259 );
260 notify_change(
261 ROOT_ENDPOINT_ID,
262 crate::dm::clusters::decl::network_commissioning::FULL_CLUSTER.id,
263 );
264
265 Ok(removed_fabric)
266 }
267
268 pub fn arm(
269 &mut self,
270 timeout_secs: u16,
271 breadcrumb: u64,
272 session_mode: &SessionMode,
273 pase: &mut Pase,
274 ) -> Result<(), Error> {
275 if matches!(self.state, State::Idle) {
276 if matches!(session_mode, SessionMode::PlainText) {
277 return Err(ErrorCode::GennCommInvalidAuthentication.into());
279 }
280
281 if pase.comm_window().is_some() && matches!(session_mode, SessionMode::Case { .. }) {
282 return Err(ErrorCode::Busy.into());
284 }
285
286 self.state = State::Armed(ArmedCtx {
292 armed_at: Instant::now(),
293 timeout_secs,
294 fab_idx: session_mode.fab_idx(),
295 flags: NocFlags::empty(),
296 });
297 self.breadcrumb = breadcrumb;
298
299 return Ok(());
300 }
301
302 self.check_state(
305 session_mode,
306 NocFlags::empty(),
307 NocFlags::empty(),
308 NocFlags::empty(),
309 )?;
310
311 let State::Armed(ctx) = &mut self.state else {
312 unreachable!();
314 };
315
316 if timeout_secs > 0 {
317 ctx.armed_at = Instant::now();
318 ctx.timeout_secs = timeout_secs;
319 self.breadcrumb = breadcrumb;
320 } else {
321 self.state = State::Idle;
323 self.breadcrumb = 0;
324 }
325
326 Ok(())
327 }
328
329 pub fn disarm<'a>(
330 &mut self,
331 session_mode: &SessionMode,
332 fabrics: &'a mut Fabrics,
333 ) -> Result<&'a mut Fabric, Error> {
334 if matches!(self.state, State::Idle) {
335 error!("Received Fail-Safe Disarm without it being armed");
336 return Err(ErrorCode::FailSafeRequired.into());
337 }
338
339 let fab_idx = Self::get_case_fab_idx(session_mode)?;
341
342 self.check_state(
343 session_mode,
344 NocFlags::empty(),
345 NocFlags::empty(),
346 NocFlags::empty(),
347 )?;
348
349 let fabric = fabrics.fabric_mut(fab_idx)?;
350
351 self.state = State::Idle;
352 self.breadcrumb = 0;
353
354 Ok(fabric)
355 }
356
357 pub fn is_armed(&self) -> bool {
358 matches!(self.state, State::Armed(_))
359 }
360
361 pub fn pending_root_ca(&self) -> Option<&[u8]> {
371 let State::Armed(ctx) = &self.state else {
372 return None;
373 };
374
375 if !ctx.flags.contains(NocFlags::ADD_ROOT_CERT_RECVD) {
376 return None;
377 }
378
379 if ctx
380 .flags
381 .intersects(NocFlags::ADD_NOC_RECVD | NocFlags::UPDATE_NOC_RECVD)
382 {
383 return None;
384 }
385
386 (!self.root_ca.is_empty()).then_some(self.root_ca.as_slice())
387 }
388
389 pub fn is_armed_for(&self, caller_fab_idx: u8) -> bool {
390 match self.state {
391 State::Idle => false,
392 State::Armed(ArmedCtx { fab_idx, .. }) => fab_idx == caller_fab_idx,
393 }
394 }
395
396 pub fn has_pending_noc_for(&self, caller_fab_idx: NonZeroU8) -> bool {
402 let State::Armed(ctx) = &self.state else {
403 return false;
404 };
405 ctx.fab_idx == caller_fab_idx.get()
406 && ctx
407 .flags
408 .intersects(NocFlags::ADD_NOC_RECVD | NocFlags::UPDATE_NOC_RECVD)
409 }
410
411 pub fn check_armed(&self, session_mode: &SessionMode) -> Result<(), Error> {
412 self.check_state(
413 session_mode,
414 NocFlags::empty(),
415 NocFlags::empty(),
416 NocFlags::empty(),
417 )
418 }
419
420 pub fn add_trusted_root_cert<C: Crypto>(
421 &mut self,
422 crypto: C,
423 time: UtcTime,
424 session_mode: &SessionMode,
425 root_ca: &[u8],
426 buf: &mut [u8],
427 ) -> Result<(), Error> {
428 self.check_state(
429 session_mode,
430 NocFlags::empty(),
431 NocFlags::ADD_ROOT_CERT_RECVD,
432 NocFlags::ADD_ROOT_CERT_RECVD,
433 )?;
434
435 {
442 let root_ref = CertRef::new(TLVElement::new(root_ca));
443 root_ref
444 .verify_chain_start(&crypto, time)
445 .finalise(buf)
446 .map_err(|_| ErrorCode::InvalidCommand)?;
447
448 if let Some(path_len) = root_ref
454 .basic_constraints_path_len()
455 .map_err(|_| ErrorCode::InvalidCommand)?
456 {
457 if path_len > 1 {
458 Err(ErrorCode::InvalidCommand)?;
459 }
460 }
461 }
462
463 self.root_ca.clear();
464 self.root_ca
465 .extend_from_slice(root_ca)
466 .map_err(|_| ErrorCode::InvalidCommand)?;
467
468 self.add_flags(NocFlags::ADD_ROOT_CERT_RECVD);
469
470 Ok(())
471 }
472
473 pub fn add_csr_req<C: Crypto>(
474 &mut self,
475 crypto: C,
476 session_mode: &SessionMode,
477 ) -> Result<CanonPkcSecretKeyRef<'_>, Error> {
478 self.check_state(
479 session_mode,
480 NocFlags::empty(),
481 NocFlags::ADD_CSR_REQ_RECVD | NocFlags::UPDATE_CSR_REQ_RECVD,
482 NocFlags::ADD_CSR_REQ_RECVD,
483 )?;
484
485 let crypto_secret_key = crypto.generate_secret_key()?;
486 crypto_secret_key.write_canon(&mut self.secret_key)?;
487
488 self.add_flags(NocFlags::ADD_CSR_REQ_RECVD);
489
490 Ok(self.secret_key.reference())
491 }
492
493 pub fn update_csr_req<C: Crypto>(
494 &mut self,
495 crypto: C,
496 session_mode: &SessionMode,
497 ) -> Result<CanonPkcSecretKeyRef<'_>, Error> {
498 Self::get_case_fab_idx(session_mode)?;
500
501 self.check_state(
502 session_mode,
503 NocFlags::empty(),
504 NocFlags::ADD_CSR_REQ_RECVD | NocFlags::UPDATE_CSR_REQ_RECVD,
505 NocFlags::UPDATE_CSR_REQ_RECVD,
506 )?;
507
508 crypto
509 .generate_secret_key()?
510 .write_canon(&mut self.secret_key)?;
511
512 self.add_flags(NocFlags::UPDATE_CSR_REQ_RECVD);
513
514 Ok(self.secret_key.reference())
515 }
516
517 #[allow(clippy::too_many_arguments)]
518 pub fn update_noc<'a, C: Crypto>(
519 &mut self,
520 crypto: C,
521 time: UtcTime,
522 fabrics: &'a mut Fabrics,
523 session_mode: &SessionMode,
524 icac: Option<&[u8]>,
525 noc: &[u8],
526 buf: &mut [u8],
527 mut mdns_notif: impl FnMut(),
528 ) -> Result<&'a mut Fabric, Error> {
529 let fab_idx = Self::get_case_fab_idx(session_mode)?;
530
531 self.check_state(
540 session_mode,
541 NocFlags::UPDATE_CSR_REQ_RECVD,
542 NocFlags::ADD_ROOT_CERT_RECVD
543 | NocFlags::ADD_NOC_RECVD
544 | NocFlags::ADD_CSR_REQ_RECVD
545 | NocFlags::UPDATE_NOC_RECVD,
546 NocFlags::UPDATE_NOC_RECVD,
547 )?;
548
549 {
550 let noc_ref = CertRef::new(TLVElement::new(noc));
551 let icac_ref = icac.map(|icac| CertRef::new(TLVElement::new(icac)));
552 let fabric_root_ca = fabrics.fabric(fab_idx)?.root_ca();
556 let root_ref = CertRef::new(TLVElement::new(fabric_root_ca));
557
558 Self::validate_certs(&crypto, time, &noc_ref, icac_ref.as_ref(), &root_ref, buf)
563 .map_err(|_| ErrorCode::NocInvalidNoc)?;
564
565 let mut csr_pubkey = crate::crypto::CanonPkcPublicKey::new();
569 crypto
570 .secret_key(self.secret_key.reference())?
571 .pub_key()?
572 .write_canon(&mut csr_pubkey)?;
573 if csr_pubkey.access().as_slice() != noc_ref.pubkey()? {
574 Err(ErrorCode::NocInvalidPublicKey)?;
575 }
576
577 let fabric_id = noc_ref.get_fabric_id()?;
582 let fabric = fabrics.fabric(fab_idx)?;
583
584 if fabric_id != fabric.fabric_id() {
585 Err(ErrorCode::NocFabricConflict)?;
586 }
587 }
588
589 let fabric = fabrics.update(
593 &crypto,
594 fab_idx,
595 self.secret_key.reference(),
596 noc,
597 icac.unwrap_or(&[]),
598 )?;
599
600 let State::Armed(ctx) = &mut self.state else {
601 unreachable!();
604 };
605
606 ctx.fab_idx = fabric.fab_idx().get();
607 self.add_flags(NocFlags::UPDATE_NOC_RECVD);
608
609 mdns_notif();
610
611 Ok(fabric)
612 }
613
614 #[allow(clippy::too_many_arguments)]
615 pub fn add_noc<'a, C: Crypto>(
616 &mut self,
617 crypto: C,
618 time: UtcTime,
619 fabrics: &'a mut Fabrics,
620 session_mode: &SessionMode,
621 vendor_id: u16,
622 icac: Option<&[u8]>,
623 noc: &[u8],
624 ipk: &[u8],
625 case_admin_subject: u64,
626 buf: &mut [u8],
627 mut mdns_notif: impl FnMut(),
628 ) -> Result<&'a mut Fabric, Error> {
629 self.check_state(
630 session_mode,
631 NocFlags::ADD_ROOT_CERT_RECVD | NocFlags::ADD_CSR_REQ_RECVD,
632 NocFlags::ADD_NOC_RECVD | NocFlags::UPDATE_CSR_REQ_RECVD | NocFlags::UPDATE_NOC_RECVD,
633 NocFlags::ADD_NOC_RECVD,
634 )?;
635
636 if !crate::acl::is_node(case_admin_subject) && !crate::acl::is_noc_cat(case_admin_subject) {
641 Err(ErrorCode::NocInvalidAdminSubject)?;
642 }
643
644 {
645 let noc_ref = CertRef::new(TLVElement::new(noc));
646 let icac_ref = icac.map(|icac| CertRef::new(TLVElement::new(icac)));
647 let root_ref = CertRef::new(TLVElement::new(&self.root_ca));
648
649 Self::validate_certs(&crypto, time, &noc_ref, icac_ref.as_ref(), &root_ref, buf)
654 .map_err(|_| ErrorCode::NocInvalidNoc)?;
655
656 let mut csr_pubkey = crate::crypto::CanonPkcPublicKey::new();
661 crypto
662 .secret_key(self.secret_key.reference())?
663 .pub_key()?
664 .write_canon(&mut csr_pubkey)?;
665 if csr_pubkey.access().as_slice() != noc_ref.pubkey()? {
666 Err(ErrorCode::NocInvalidPublicKey)?;
667 }
668
669 let fabric_id = noc_ref.get_fabric_id()?;
673 let root_cert_pubkey = root_ref.pubkey()?;
674
675 for fabric in fabrics.iter() {
676 if fabric_id == fabric.fabric_id() {
677 let f_root_ref = CertRef::new(TLVElement::new(fabric.root_ca()));
678 let f_root_pubkey = f_root_ref.pubkey()?;
679
680 if root_cert_pubkey == f_root_pubkey {
681 Err(ErrorCode::NocFabricConflict)?;
684 }
685 }
686 }
687 }
688
689 let fabric = fabrics
690 .add(
691 &crypto,
692 self.secret_key.reference(),
693 &self.root_ca,
694 noc,
695 icac.unwrap_or(&[]),
696 Some(CanonAeadKeyRef::try_new(ipk)?),
697 vendor_id,
698 case_admin_subject,
699 )
700 .map_err(|e| {
701 if e.code() == ErrorCode::ResourceExhausted {
702 ErrorCode::NocFabricTableFull.into()
703 } else {
704 e
705 }
706 })?;
707
708 info!(
709 "Added operational fabric with local index {}",
710 fabric.fab_idx()
711 );
712
713 let State::Armed(ctx) = &mut self.state else {
714 unreachable!();
717 };
718
719 ctx.fab_idx = fabric.fab_idx().get();
720 self.add_flags(NocFlags::ADD_NOC_RECVD);
721
722 mdns_notif();
723
724 Ok(fabric)
725 }
726
727 pub fn breadcrumb(&self) -> u64 {
728 self.breadcrumb
729 }
730
731 pub fn set_breadcrumb(&mut self, value: u64) {
732 self.breadcrumb = value;
733 }
734
735 #[allow(clippy::too_many_arguments)]
736 fn validate_certs<C: Crypto>(
737 crypto: C,
738 time: UtcTime,
739 noc: &CertRef,
740 icac: Option<&CertRef>,
741 root: &CertRef,
742 buf: &mut [u8],
743 ) -> Result<(), Error> {
744 let mut verifier = noc.verify_chain_start(crypto, time);
745
746 if let Some(icac) = icac {
747 if icac.is_self_signed()? {
752 return Err(ErrorCode::InvalidData.into());
753 }
754 verifier = verifier.add_cert(icac, buf)?;
755 }
756
757 verifier.add_cert(root, buf)?.finalise(buf)
758 }
759
760 fn get_case_fab_idx(session_mode: &SessionMode) -> Result<NonZeroU8, Error> {
761 if let SessionMode::Case { fab_idx, .. } = session_mode {
762 Ok(*fab_idx)
763 } else {
764 Err(ErrorCode::GennCommInvalidAuthentication.into())
766 }
767 }
768
769 fn check_state(
770 &self,
771 session_mode: &SessionMode,
772 present: NocFlags,
773 absent: NocFlags,
774 op: NocFlags,
775 ) -> Result<(), Error> {
776 if let State::Armed(ctx) = &self.state {
777 if matches!(session_mode, SessionMode::PlainText) {
778 Err(ErrorCode::GennCommInvalidAuthentication)?;
780 }
781
782 if op == NocFlags::UPDATE_NOC_RECVD && !matches!(session_mode, SessionMode::Case { .. })
783 {
784 Err(ErrorCode::GennCommInvalidAuthentication)?;
786 }
787
788 if ctx.fab_idx != session_mode.fab_idx() {
789 Err(ErrorCode::NocInvalidFabricIndex)?;
791 }
792
793 if !ctx.flags.contains(present) {
794 let any_csr = ctx
805 .flags
806 .intersects(NocFlags::ADD_CSR_REQ_RECVD | NocFlags::UPDATE_CSR_REQ_RECVD);
807 if (op == NocFlags::ADD_NOC_RECVD || op == NocFlags::UPDATE_NOC_RECVD) && !any_csr {
808 Err(ErrorCode::NocMissingCsr)?;
809 }
810
811 Err(ErrorCode::ConstraintError)?;
812 }
813
814 if !ctx.flags.intersection(absent).is_empty() {
815 Err(ErrorCode::ConstraintError)?;
825 }
826 } else {
827 Err(ErrorCode::FailSafeRequired)?;
829 }
830
831 Ok(())
832 }
833
834 fn add_flags(&mut self, flags: NocFlags) {
835 match &mut self.state {
836 State::Armed(ctx) => ctx.flags |= flags,
837 _ => panic!("Not armed"),
838 }
839 }
840}
841
842impl Default for FailSafe {
843 fn default() -> Self {
844 Self::new()
845 }
846}