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)]
124 pub fn check_failsafe_timeout<S, N>(
125 &mut self,
126 fabrics: &mut Fabrics,
127 sessions: &mut crate::transport::session::Sessions,
128 networks: N,
129 kv: S,
130 expire_sess_id: Option<u32>,
131 mdns_notif: impl FnMut(),
132 notify_change: impl FnMut(EndptId, ClusterId),
133 ) -> Result<bool, Error>
134 where
135 S: KvBlobStoreAccess,
136 N: NetworksAccess,
137 {
138 if let State::Armed(ctx) = &self.state {
139 let now = Instant::now();
140 if now
141 >= ctx
142 .armed_at
143 .saturating_add(Duration::from_secs(ctx.timeout_secs as u64))
144 {
145 self.expire(
149 fabrics,
150 sessions,
151 expire_sess_id,
152 networks,
153 kv,
154 mdns_notif,
155 notify_change,
156 )?;
157 return Ok(true);
158 }
159 }
160
161 Ok(false)
162 }
163
164 #[allow(clippy::too_many_arguments)]
174 pub fn expire<S, N>(
175 &mut self,
176 fabrics: &mut Fabrics,
177 sessions: &mut crate::transport::session::Sessions,
178 expire_sess_id: Option<u32>,
179 networks: N,
180 kv: S,
181 mut mdns_notif: impl FnMut(),
182 mut notify_change: impl FnMut(EndptId, ClusterId),
183 ) -> Result<bool, Error>
184 where
185 S: KvBlobStoreAccess,
186 N: NetworksAccess,
187 {
188 let State::Armed(ctx) = &self.state else {
189 return Ok(false);
190 };
191
192 warn!(
193 "Fail-Safe timeout expired for fabric {}, disarming",
194 ctx.fab_idx
195 );
196
197 let fab_idx_raw = ctx.fab_idx;
198
199 kv.access(|mut kv, buf| {
200 if let Some(fab_idx) = NonZeroU8::new(fab_idx_raw) {
201 fabrics.remove(fab_idx)?;
202 fabrics.add_load(fab_idx.get(), &mut kv, buf)?;
203 }
204
205 networks.access(|networks| {
206 let data = kv.load(NETWORKS_KEY, buf)?;
207
208 if let Some(data) = data {
209 networks.load(data)
210 } else {
211 networks.reset()
212 }
213 })
214 })?;
215
216 sessions.remove_pase(expire_sess_id);
224
225 self.state = State::Idle;
226 self.breadcrumb = 0;
227
228 mdns_notif();
229
230 notify_change(
242 ROOT_ENDPOINT_ID,
243 crate::dm::clusters::decl::operational_credentials::FULL_CLUSTER.id,
244 );
245 notify_change(
246 ROOT_ENDPOINT_ID,
247 crate::dm::clusters::decl::network_commissioning::FULL_CLUSTER.id,
248 );
249
250 Ok(true)
251 }
252
253 pub fn arm(
254 &mut self,
255 timeout_secs: u16,
256 breadcrumb: u64,
257 session_mode: &SessionMode,
258 pase: &mut Pase,
259 ) -> Result<(), Error> {
260 if matches!(self.state, State::Idle) {
261 if matches!(session_mode, SessionMode::PlainText) {
262 return Err(ErrorCode::GennCommInvalidAuthentication)?;
264 }
265
266 if pase.comm_window().is_some() && matches!(session_mode, SessionMode::Case { .. }) {
267 return Err(ErrorCode::Busy)?;
269 }
270
271 self.state = State::Armed(ArmedCtx {
277 armed_at: Instant::now(),
278 timeout_secs,
279 fab_idx: session_mode.fab_idx(),
280 flags: NocFlags::empty(),
281 });
282 self.breadcrumb = breadcrumb;
283
284 return Ok(());
285 }
286
287 self.check_state(
290 session_mode,
291 NocFlags::empty(),
292 NocFlags::empty(),
293 NocFlags::empty(),
294 )?;
295
296 let State::Armed(ctx) = &mut self.state else {
297 unreachable!();
299 };
300
301 if timeout_secs > 0 {
302 ctx.armed_at = Instant::now();
303 ctx.timeout_secs = timeout_secs;
304 self.breadcrumb = breadcrumb;
305 } else {
306 self.state = State::Idle;
308 self.breadcrumb = 0;
309 }
310
311 Ok(())
312 }
313
314 pub fn disarm<'a>(
315 &mut self,
316 session_mode: &SessionMode,
317 fabrics: &'a mut Fabrics,
318 ) -> Result<&'a mut Fabric, Error> {
319 if matches!(self.state, State::Idle) {
320 error!("Received Fail-Safe Disarm without it being armed");
321 return Err(ErrorCode::FailSafeRequired)?;
322 }
323
324 let fab_idx = Self::get_case_fab_idx(session_mode)?;
326
327 self.check_state(
328 session_mode,
329 NocFlags::empty(),
330 NocFlags::empty(),
331 NocFlags::empty(),
332 )?;
333
334 let fabric = fabrics.fabric_mut(fab_idx)?;
335
336 self.state = State::Idle;
337 self.breadcrumb = 0;
338
339 Ok(fabric)
340 }
341
342 pub fn is_armed(&self) -> bool {
343 matches!(self.state, State::Armed(_))
344 }
345
346 pub fn pending_root_ca(&self) -> Option<&[u8]> {
356 let State::Armed(ctx) = &self.state else {
357 return None;
358 };
359
360 if !ctx.flags.contains(NocFlags::ADD_ROOT_CERT_RECVD) {
361 return None;
362 }
363
364 if ctx
365 .flags
366 .intersects(NocFlags::ADD_NOC_RECVD | NocFlags::UPDATE_NOC_RECVD)
367 {
368 return None;
369 }
370
371 (!self.root_ca.is_empty()).then_some(self.root_ca.as_slice())
372 }
373
374 pub fn is_armed_for(&self, caller_fab_idx: u8) -> bool {
375 match self.state {
376 State::Idle => false,
377 State::Armed(ArmedCtx { fab_idx, .. }) => fab_idx == caller_fab_idx,
378 }
379 }
380
381 pub fn has_pending_noc_for(&self, caller_fab_idx: NonZeroU8) -> bool {
387 let State::Armed(ctx) = &self.state else {
388 return false;
389 };
390 ctx.fab_idx == caller_fab_idx.get()
391 && ctx
392 .flags
393 .intersects(NocFlags::ADD_NOC_RECVD | NocFlags::UPDATE_NOC_RECVD)
394 }
395
396 pub fn check_armed(&self, session_mode: &SessionMode) -> Result<(), Error> {
397 self.check_state(
398 session_mode,
399 NocFlags::empty(),
400 NocFlags::empty(),
401 NocFlags::empty(),
402 )
403 }
404
405 pub fn add_trusted_root_cert<C: Crypto>(
406 &mut self,
407 crypto: C,
408 time: UtcTime,
409 session_mode: &SessionMode,
410 root_ca: &[u8],
411 buf: &mut [u8],
412 ) -> Result<(), Error> {
413 self.check_state(
414 session_mode,
415 NocFlags::empty(),
416 NocFlags::ADD_ROOT_CERT_RECVD,
417 NocFlags::ADD_ROOT_CERT_RECVD,
418 )?;
419
420 {
427 let root_ref = CertRef::new(TLVElement::new(root_ca));
428 root_ref
429 .verify_chain_start(&crypto, time)
430 .finalise(buf)
431 .map_err(|_| ErrorCode::InvalidCommand)?;
432
433 if let Some(path_len) = root_ref
439 .basic_constraints_path_len()
440 .map_err(|_| ErrorCode::InvalidCommand)?
441 {
442 if path_len > 1 {
443 Err(ErrorCode::InvalidCommand)?;
444 }
445 }
446 }
447
448 self.root_ca.clear();
449 self.root_ca
450 .extend_from_slice(root_ca)
451 .map_err(|_| ErrorCode::InvalidCommand)?;
452
453 self.add_flags(NocFlags::ADD_ROOT_CERT_RECVD);
454
455 Ok(())
456 }
457
458 pub fn add_csr_req<C: Crypto>(
459 &mut self,
460 crypto: C,
461 session_mode: &SessionMode,
462 ) -> Result<CanonPkcSecretKeyRef<'_>, Error> {
463 self.check_state(
464 session_mode,
465 NocFlags::empty(),
466 NocFlags::ADD_CSR_REQ_RECVD | NocFlags::UPDATE_CSR_REQ_RECVD,
467 NocFlags::ADD_CSR_REQ_RECVD,
468 )?;
469
470 let crypto_secret_key = crypto.generate_secret_key()?;
471 crypto_secret_key.write_canon(&mut self.secret_key)?;
472
473 self.add_flags(NocFlags::ADD_CSR_REQ_RECVD);
474
475 Ok(self.secret_key.reference())
476 }
477
478 pub fn update_csr_req<C: Crypto>(
479 &mut self,
480 crypto: C,
481 session_mode: &SessionMode,
482 ) -> Result<CanonPkcSecretKeyRef<'_>, Error> {
483 Self::get_case_fab_idx(session_mode)?;
485
486 self.check_state(
487 session_mode,
488 NocFlags::empty(),
489 NocFlags::ADD_CSR_REQ_RECVD | NocFlags::UPDATE_CSR_REQ_RECVD,
490 NocFlags::UPDATE_CSR_REQ_RECVD,
491 )?;
492
493 crypto
494 .generate_secret_key()?
495 .write_canon(&mut self.secret_key)?;
496
497 self.add_flags(NocFlags::UPDATE_CSR_REQ_RECVD);
498
499 Ok(self.secret_key.reference())
500 }
501
502 #[allow(clippy::too_many_arguments)]
503 pub fn update_noc<'a, C: Crypto>(
504 &mut self,
505 crypto: C,
506 time: UtcTime,
507 fabrics: &'a mut Fabrics,
508 session_mode: &SessionMode,
509 icac: Option<&[u8]>,
510 noc: &[u8],
511 buf: &mut [u8],
512 mut mdns_notif: impl FnMut(),
513 ) -> Result<&'a mut Fabric, Error> {
514 let fab_idx = Self::get_case_fab_idx(session_mode)?;
515
516 self.check_state(
525 session_mode,
526 NocFlags::UPDATE_CSR_REQ_RECVD,
527 NocFlags::ADD_ROOT_CERT_RECVD
528 | NocFlags::ADD_NOC_RECVD
529 | NocFlags::ADD_CSR_REQ_RECVD
530 | NocFlags::UPDATE_NOC_RECVD,
531 NocFlags::UPDATE_NOC_RECVD,
532 )?;
533
534 {
535 let noc_ref = CertRef::new(TLVElement::new(noc));
536 let icac_ref = icac.map(|icac| CertRef::new(TLVElement::new(icac)));
537 let fabric_root_ca = fabrics.fabric(fab_idx)?.root_ca();
541 let root_ref = CertRef::new(TLVElement::new(fabric_root_ca));
542
543 Self::validate_certs(&crypto, time, &noc_ref, icac_ref.as_ref(), &root_ref, buf)
548 .map_err(|_| ErrorCode::NocInvalidNoc)?;
549
550 let mut csr_pubkey = crate::crypto::CanonPkcPublicKey::new();
554 crypto
555 .secret_key(self.secret_key.reference())?
556 .pub_key()?
557 .write_canon(&mut csr_pubkey)?;
558 if csr_pubkey.access().as_slice() != noc_ref.pubkey()? {
559 Err(ErrorCode::NocInvalidPublicKey)?;
560 }
561
562 let fabric_id = noc_ref.get_fabric_id()?;
567 let fabric = fabrics.fabric(fab_idx)?;
568
569 if fabric_id != fabric.fabric_id() {
570 Err(ErrorCode::NocFabricConflict)?;
571 }
572 }
573
574 let fabric = fabrics.update(
578 &crypto,
579 fab_idx,
580 self.secret_key.reference(),
581 noc,
582 icac.unwrap_or(&[]),
583 )?;
584
585 let State::Armed(ctx) = &mut self.state else {
586 unreachable!();
589 };
590
591 ctx.fab_idx = fabric.fab_idx().get();
592 self.add_flags(NocFlags::UPDATE_NOC_RECVD);
593
594 mdns_notif();
595
596 Ok(fabric)
597 }
598
599 #[allow(clippy::too_many_arguments)]
600 pub fn add_noc<'a, C: Crypto>(
601 &mut self,
602 crypto: C,
603 time: UtcTime,
604 fabrics: &'a mut Fabrics,
605 session_mode: &SessionMode,
606 vendor_id: u16,
607 icac: Option<&[u8]>,
608 noc: &[u8],
609 ipk: &[u8],
610 case_admin_subject: u64,
611 buf: &mut [u8],
612 mut mdns_notif: impl FnMut(),
613 ) -> Result<&'a mut Fabric, Error> {
614 self.check_state(
615 session_mode,
616 NocFlags::ADD_ROOT_CERT_RECVD | NocFlags::ADD_CSR_REQ_RECVD,
617 NocFlags::ADD_NOC_RECVD | NocFlags::UPDATE_CSR_REQ_RECVD | NocFlags::UPDATE_NOC_RECVD,
618 NocFlags::ADD_NOC_RECVD,
619 )?;
620
621 if !crate::acl::is_node(case_admin_subject) && !crate::acl::is_noc_cat(case_admin_subject) {
626 Err(ErrorCode::NocInvalidAdminSubject)?;
627 }
628
629 {
630 let noc_ref = CertRef::new(TLVElement::new(noc));
631 let icac_ref = icac.map(|icac| CertRef::new(TLVElement::new(icac)));
632 let root_ref = CertRef::new(TLVElement::new(&self.root_ca));
633
634 Self::validate_certs(&crypto, time, &noc_ref, icac_ref.as_ref(), &root_ref, buf)
639 .map_err(|_| ErrorCode::NocInvalidNoc)?;
640
641 let mut csr_pubkey = crate::crypto::CanonPkcPublicKey::new();
646 crypto
647 .secret_key(self.secret_key.reference())?
648 .pub_key()?
649 .write_canon(&mut csr_pubkey)?;
650 if csr_pubkey.access().as_slice() != noc_ref.pubkey()? {
651 Err(ErrorCode::NocInvalidPublicKey)?;
652 }
653
654 let fabric_id = noc_ref.get_fabric_id()?;
658 let root_cert_pubkey = root_ref.pubkey()?;
659
660 for fabric in fabrics.iter() {
661 if fabric_id == fabric.fabric_id() {
662 let f_root_ref = CertRef::new(TLVElement::new(fabric.root_ca()));
663 let f_root_pubkey = f_root_ref.pubkey()?;
664
665 if root_cert_pubkey == f_root_pubkey {
666 Err(ErrorCode::NocFabricConflict)?;
669 }
670 }
671 }
672 }
673
674 let fabric = fabrics
675 .add(
676 &crypto,
677 self.secret_key.reference(),
678 &self.root_ca,
679 noc,
680 icac.unwrap_or(&[]),
681 Some(CanonAeadKeyRef::try_new(ipk)?),
682 vendor_id,
683 case_admin_subject,
684 )
685 .map_err(|e| {
686 if e.code() == ErrorCode::ResourceExhausted {
687 ErrorCode::NocFabricTableFull.into()
688 } else {
689 e
690 }
691 })?;
692
693 info!(
694 "Added operational fabric with local index {}",
695 fabric.fab_idx()
696 );
697
698 let State::Armed(ctx) = &mut self.state else {
699 unreachable!();
702 };
703
704 ctx.fab_idx = fabric.fab_idx().get();
705 self.add_flags(NocFlags::ADD_NOC_RECVD);
706
707 mdns_notif();
708
709 Ok(fabric)
710 }
711
712 pub fn breadcrumb(&self) -> u64 {
713 self.breadcrumb
714 }
715
716 pub fn set_breadcrumb(&mut self, value: u64) {
717 self.breadcrumb = value;
718 }
719
720 #[allow(clippy::too_many_arguments)]
721 fn validate_certs<C: Crypto>(
722 crypto: C,
723 time: UtcTime,
724 noc: &CertRef,
725 icac: Option<&CertRef>,
726 root: &CertRef,
727 buf: &mut [u8],
728 ) -> Result<(), Error> {
729 let mut verifier = noc.verify_chain_start(crypto, time);
730
731 if let Some(icac) = icac {
732 if icac.is_self_signed()? {
737 return Err(ErrorCode::InvalidData.into());
738 }
739 verifier = verifier.add_cert(icac, buf)?;
740 }
741
742 verifier.add_cert(root, buf)?.finalise(buf)
743 }
744
745 fn get_case_fab_idx(session_mode: &SessionMode) -> Result<NonZeroU8, Error> {
746 if let SessionMode::Case { fab_idx, .. } = session_mode {
747 Ok(*fab_idx)
748 } else {
749 Err(ErrorCode::GennCommInvalidAuthentication.into())
751 }
752 }
753
754 fn check_state(
755 &self,
756 session_mode: &SessionMode,
757 present: NocFlags,
758 absent: NocFlags,
759 op: NocFlags,
760 ) -> Result<(), Error> {
761 if let State::Armed(ctx) = &self.state {
762 if matches!(session_mode, SessionMode::PlainText) {
763 Err(ErrorCode::GennCommInvalidAuthentication)?;
765 }
766
767 if op == NocFlags::UPDATE_NOC_RECVD && !matches!(session_mode, SessionMode::Case { .. })
768 {
769 Err(ErrorCode::GennCommInvalidAuthentication)?;
771 }
772
773 if ctx.fab_idx != session_mode.fab_idx() {
774 Err(ErrorCode::NocInvalidFabricIndex)?;
776 }
777
778 if !ctx.flags.contains(present) {
779 let any_csr = ctx
790 .flags
791 .intersects(NocFlags::ADD_CSR_REQ_RECVD | NocFlags::UPDATE_CSR_REQ_RECVD);
792 if (op == NocFlags::ADD_NOC_RECVD || op == NocFlags::UPDATE_NOC_RECVD) && !any_csr {
793 Err(ErrorCode::NocMissingCsr)?;
794 }
795
796 Err(ErrorCode::ConstraintError)?;
797 }
798
799 if !ctx.flags.intersection(absent).is_empty() {
800 Err(ErrorCode::ConstraintError)?;
810 }
811 } else {
812 Err(ErrorCode::FailSafeRequired)?;
814 }
815
816 Ok(())
817 }
818
819 fn add_flags(&mut self, flags: NocFlags) {
820 match &mut self.state {
821 State::Armed(ctx) => ctx.flags |= flags,
822 _ => panic!("Not armed"),
823 }
824 }
825}
826
827impl Default for FailSafe {
828 fn default() -> Self {
829 Self::new()
830 }
831}