1use core::mem::MaybeUninit;
24use core::num::NonZeroU8;
25
26use crate::alloc;
27use crate::cert::CertRef;
28#[cfg(feature = "case-resumption")]
29use crate::crypto::CanonAeadKey;
30use crate::crypto::{
31 CanonPkcPublicKeyRef, CanonPkcSignature, CanonPkcSignatureRef, Crypto, Hash,
32 AEAD_CANON_KEY_LEN, AEAD_TAG_LEN,
33};
34use crate::error::{Error, ErrorCode};
35use crate::sc::{complete_with_status, GeneralCode, OpCode, SCStatusCodes, StatusReport};
36use crate::tlv::{get_root_node_struct, FromTLV, OctetStr, TLVElement, TLVTag, TLVWrite};
37use crate::transport::exchange::Exchange;
38use crate::transport::session::{NocCatIds, ReservedSession, SessionMode};
39use crate::utils::init::InitMaybeUninit;
40use crate::utils::storage::ReadBuf;
41
42#[cfg(feature = "case-resumption")]
43use super::casep::{
44 compute_resume_mic, compute_resumption_session_keys, derive_resume_key, verify_resume_mic,
45 ResumeKeyKind, RESUME1_MIC_NONCE, RESUME2_MIC_NONCE,
46};
47use super::casep::{
48 CaseP, CaseRandom, CaseRandomRef, CaseSessionKeys, CASE_RESUMPTION_ID_LEN,
49 CASE_RESUMPTION_ID_ZEROED,
50};
51#[cfg(feature = "case-resumption")]
52use super::resumption::ResumableSession;
53use super::CASE_LARGE_BUF_SIZE;
54
55#[derive(FromTLV, Debug)]
57#[cfg_attr(feature = "defmt", derive(defmt::Format))]
58#[tlvargs(start = 1, lifetime = "'a")]
59struct Sigma2Resp<'a> {
60 responder_random: OctetStr<'a>,
62 responder_sessid: u16,
64 responder_eph_pub_key: OctetStr<'a>,
66 encrypted2: OctetStr<'a>,
68}
69
70#[derive(FromTLV)]
72#[tlvargs(start = 1, lifetime = "'a")]
73struct TBEData2Decrypt<'a> {
74 responder_noc: OctetStr<'a>,
75 responder_icac: Option<OctetStr<'a>>,
76 signature: OctetStr<'a>,
77 resumption_id: OctetStr<'a>,
78}
79
80#[cfg(feature = "case-resumption")]
83#[derive(FromTLV, Debug)]
84#[cfg_attr(feature = "defmt", derive(defmt::Format))]
85#[tlvargs(start = 1, lifetime = "'a")]
86struct Sigma2ResumeMsg<'a> {
87 resumption_id: OctetStr<'a>,
89 sigma2_resume_mic: OctetStr<'a>,
91 responder_sessid: u16,
93 _session_parameters: Option<crate::sc::SessionParameters>,
97}
98
99pub struct CaseInitiator<'a, C: Crypto + 'a> {
109 casep: CaseP<'a, C>,
110 peer_node_id: u64,
112 secret_key: Option<C::SecretKey<'a>>,
114}
115
116impl<'a, C: Crypto + 'a> CaseInitiator<'a, C> {
117 const fn new(peer_node_id: u64) -> Self {
119 Self {
120 casep: CaseP::new(),
121 peer_node_id,
122 secret_key: None,
123 }
124 }
125
126 pub async fn perform(
142 mut exchange: Exchange<'_>,
143 crypto: &'a C,
144 fab_idx: NonZeroU8,
145 peer_node_id: u64,
146 ) -> Result<(), Error> {
147 let mut session = ReservedSession::reserve(exchange.matter(), crypto).await?;
149
150 let mut initiator = Self::new(peer_node_id);
151
152 #[cfg(feature = "case-resumption")]
161 let cached_record: Option<ResumableSession> = exchange.with_state(|state| {
162 Ok(state
163 .resumption
164 .find_by_peer(fab_idx, peer_node_id)
165 .cloned())
166 })?;
167
168 let mut random = MaybeUninit::<CaseRandom>::uninit();
169 let random = random.init_with(CaseRandom::init());
170
171 let mut dest_id = MaybeUninit::<Hash>::uninit();
172 let dest_id = dest_id.init_with(Hash::init());
173
174 let local_sessid = exchange.with_state(|state| {
176 let local_sessid = state.sessions.get_next_sess_id();
177
178 let fabric = state.fabrics.fabric(fab_idx)?;
179
180 let secret_key = initiator.casep.start_initiator(
181 crypto,
182 fabric,
183 peer_node_id,
184 local_sessid,
185 random,
186 dest_id,
187 )?;
188
189 initiator.secret_key = Some(secret_key);
190
191 Ok(local_sessid)
192 })?;
193
194 #[allow(clippy::type_complexity)]
202 let (resume_rid_bytes, resume_mic_bytes): (
203 Option<[u8; CASE_RESUMPTION_ID_LEN]>,
204 Option<[u8; AEAD_TAG_LEN]>,
205 );
206 #[cfg(feature = "case-resumption")]
207 {
208 (resume_rid_bytes, resume_mic_bytes) = if let Some(ref record) = cached_record {
209 let mut s1rk = CanonAeadKey::new();
210 derive_resume_key(
211 crypto,
212 ResumeKeyKind::S1rk,
213 record.shared_secret.reference(),
214 random.reference(),
215 record.resumption_id.reference(),
216 &mut s1rk,
217 )?;
218
219 let mut mic = [0u8; AEAD_TAG_LEN];
220 compute_resume_mic(crypto, s1rk.reference(), RESUME1_MIC_NONCE, &mut mic)?;
221
222 (Some(*record.resumption_id.reference().access()), Some(mic))
223 } else {
224 (None, None)
225 };
226 }
227 #[cfg(not(feature = "case-resumption"))]
228 {
229 (resume_rid_bytes, resume_mic_bytes) = (None, None);
230 }
231
232 let mut tt_updated = false;
234 exchange
235 .send_with(|_, tw| {
236 tw.start_struct(&TLVTag::Anonymous)?;
237 tw.str(&TLVTag::Context(1), random.access())?;
238 tw.u16(&TLVTag::Context(2), local_sessid)?;
239 tw.str(&TLVTag::Context(3), dest_id.access())?;
240 tw.str(&TLVTag::Context(4), initiator.casep.our_pub_key().access())?;
241
242 if let (Some(rid), Some(mic)) =
245 (resume_rid_bytes.as_ref(), resume_mic_bytes.as_ref())
246 {
247 tw.str(&TLVTag::Context(6), rid)?;
248 tw.str(&TLVTag::Context(7), mic)?;
249 }
250
251 tw.end_container()?;
252
253 if !tt_updated {
254 initiator.casep.update_tt(tw.as_slice())?;
255 tt_updated = true;
256 }
257
258 Ok(Some(OpCode::CASESigma1.into()))
259 })
260 .await?;
261
262 exchange.recv_fetch().await?;
264
265 let response_opcode = exchange.rx()?.meta().proto_opcode;
266
267 if response_opcode == OpCode::StatusReport as u8 {
268 let rx = exchange.rx()?;
269 let mut rb = ReadBuf::new(rx.payload());
270 let status = StatusReport::read(&mut rb)?;
271 error!(
272 "CASE Sigma1 failed: general={:?}, proto_code={}",
273 status.general_code, status.proto_code
274 );
275 return Err(ErrorCode::Invalid.into());
276 }
277
278 #[cfg(feature = "case-resumption")]
282 if response_opcode == OpCode::CASESigma2Resume as u8 {
283 let Some(record) = cached_record else {
288 error!("Responder sent Sigma2_Resume but we did not request resumption");
289 complete_with_status(&mut exchange, SCStatusCodes::InvalidParameter, &[]).await?;
290 return Err(ErrorCode::Invalid.into());
291 };
292
293 return Self::finalize_sigma2_resume(
294 &mut exchange,
295 crypto,
296 session,
297 fab_idx,
298 local_sessid,
299 record,
300 random,
301 )
302 .await;
303 }
304
305 if response_opcode != OpCode::CASESigma2 as u8 {
306 error!(
307 "Unexpected opcode: expected CASESigma2 or CASESigma2Resume, got {}",
308 response_opcode
309 );
310 return Err(ErrorCode::InvalidOpcode.into());
311 }
312
313 #[cfg_attr(not(feature = "case-resumption"), allow(unused_variables))]
318 let (peer_catids, peer_resumption_id) = {
319 let rx = exchange.rx()?;
320 let raw_sigma2_payload = rx.payload();
321
322 let sigma2 = Sigma2Resp::from_tlv(&get_root_node_struct(raw_sigma2_payload)?)?;
323
324 let result = exchange.with_state(|state| {
325 let mut encrypted2_buf = alloc!([0u8; CASE_LARGE_BUF_SIZE]); if sigma2.encrypted2.0.len() > encrypted2_buf.len() {
329 error!("Sigma2 encrypted data too large");
330 return Err(ErrorCode::BufferTooSmall.into());
331 }
332
333 let encrypted2 = &mut encrypted2_buf[..sigma2.encrypted2.0.len()];
334 encrypted2.copy_from_slice(sigma2.encrypted2.0);
335
336 let peer_random = CaseRandomRef::try_new(sigma2.responder_random.0)?;
337 let peer_sessid = sigma2.responder_sessid;
338 let peer_eph_pub_key =
339 CanonPkcPublicKeyRef::try_new(sigma2.responder_eph_pub_key.0)?;
340
341 let fabric = state.fabrics.fabric(fab_idx)?;
342
343 let secret_key = initiator
344 .secret_key
345 .as_ref()
346 .ok_or(ErrorCode::InvalidState)?;
347
348 let len = initiator
350 .casep
351 .sigma2_decrypt(
352 crypto,
353 fabric,
354 secret_key,
355 raw_sigma2_payload,
356 peer_random,
357 peer_sessid,
358 peer_eph_pub_key,
359 encrypted2,
360 )
361 .inspect_err(|e| {
362 error!("Failed to decrypt Sigma2 TBE: {}", e);
363 })?;
364
365 initiator.secret_key = None;
367
368 let decrypted = &encrypted2[..len];
369 let decrypted_data = TBEData2Decrypt::from_tlv(&get_root_node_struct(decrypted)?)?;
370
371 let responder_noc = CertRef::new(TLVElement::new(decrypted_data.responder_noc.0));
373 let icac_cert = decrypted_data
374 .responder_icac
375 .as_ref()
376 .map(|icac| CertRef::new(TLVElement::new(icac.0)));
377
378 let mut tmp_buf = alloc!([0u8; CASE_LARGE_BUF_SIZE]); initiator
380 .casep
381 .validate_certs(
382 crypto,
383 state.rtc.utc_time(),
384 fabric,
385 &responder_noc,
386 icac_cert.as_ref(),
387 &mut tmp_buf[..],
388 )
389 .inspect_err(|e| {
390 error!("Certificate chain doesn't match: {}", e);
391 })?;
392
393 if responder_noc.get_node_id()? != initiator.peer_node_id {
395 error!(
396 "Responder node ID doesn't match expected peer: expected {}, got {}",
397 initiator.peer_node_id,
398 responder_noc.get_node_id()?
399 );
400
401 Err(ErrorCode::Invalid)?;
402 }
403
404 initiator
406 .casep
407 .validate_peer_tbs_signature(
408 crypto,
409 decrypted_data.responder_noc.0,
410 decrypted_data.responder_icac.map(|a| a.0),
411 &responder_noc,
412 CanonPkcSignatureRef::try_new(decrypted_data.signature.0)?,
413 &mut tmp_buf[..],
414 )
415 .inspect_err(|e| {
416 error!("Sigma2 signature doesn't match: {}", e);
417 })?;
418
419 let mut peer_catids: NocCatIds = Default::default();
421 responder_noc.get_cat_ids(&mut peer_catids)?;
422
423 let mut resumption_id = CASE_RESUMPTION_ID_ZEROED;
425 resumption_id
426 .access_mut()
427 .copy_from_slice(decrypted_data.resumption_id.0);
428
429 Ok((peer_catids, resumption_id))
430 });
431
432 if result.is_err() {
433 complete_with_status(&mut exchange, SCStatusCodes::InvalidParameter, &[]).await?;
434 }
435
436 result
437 }?;
438
439 let mut signature = MaybeUninit::<CanonPkcSignature>::uninit();
441 let signature = signature.init_with(CanonPkcSignature::init());
442
443 exchange.with_state(|state| {
444 let fabric = state.fabrics.fabric(fab_idx)?;
445
446 let mut tmp_buf = alloc!([0u8; CASE_LARGE_BUF_SIZE]);
448 initiator
449 .casep
450 .compute_sigma3_signature(crypto, fabric, &mut tmp_buf[..], signature)
451 })?;
452
453 let mut tt_updated = false;
455 exchange
456 .send_with(|exchange_ref, tw| {
457 exchange_ref.with_state(|state| {
458 let fabric = state.fabrics.fabric(fab_idx)?;
459
460 tw.start_struct(&TLVTag::Anonymous)?;
461 tw.str_cb(&TLVTag::Context(1), |buf| {
462 initiator
463 .casep
464 .sigma3_encrypt(crypto, fabric, signature.reference(), buf)
465 })?;
466 tw.end_container()?;
467
468 if !tt_updated {
469 initiator.casep.update_tt(tw.as_slice())?;
470 tt_updated = true;
471 }
472
473 Ok(Some(OpCode::CASESigma3.into()))
474 })
475 })
476 .await?;
477
478 exchange.recv_fetch().await?;
480
481 {
482 let rx = exchange.rx()?;
483 let meta = rx.meta();
484
485 if meta.proto_opcode != OpCode::StatusReport as u8 {
486 error!(
487 "Unexpected opcode: expected StatusReport, got {}",
488 meta.proto_opcode
489 );
490 return Err(ErrorCode::InvalidOpcode.into());
491 }
492
493 let mut rb = ReadBuf::new(rx.payload());
494 let status = StatusReport::read(&mut rb)?;
495
496 if status.general_code != GeneralCode::Success
497 || status.proto_code != SCStatusCodes::SessionEstablishmentSuccess as u16
498 {
499 error!(
500 "CASE failed: general={:?}, proto_code={}",
501 status.general_code, status.proto_code
502 );
503 return Err(ErrorCode::Invalid.into());
504 }
505 }
506
507 {
509 let mut session_keys = MaybeUninit::<CaseSessionKeys>::uninit();
510 let session_keys = session_keys.init_with(CaseSessionKeys::init());
511
512 let (peer_addr, local_node_id) = exchange.with_state(|state| {
513 let sess = exchange.id().session(&mut state.sessions);
514
515 let fabric = state.fabrics.fabric(fab_idx)?;
516
517 initiator.casep.compute_session_keys(
518 crypto,
519 fabric.ipk().op_key(),
520 session_keys,
521 )?;
522
523 Ok((sess.get_peer_addr(), fabric.node_id()))
524 })?;
525
526 let (enc_key, remaining) = session_keys
528 .reference()
529 .split::<AEAD_CANON_KEY_LEN, { AEAD_CANON_KEY_LEN * 2 }>();
530 let (dec_key, att_challenge) =
531 remaining.split::<AEAD_CANON_KEY_LEN, AEAD_CANON_KEY_LEN>();
532
533 session.update(
534 local_node_id,
535 peer_node_id,
536 initiator.casep.peer_sessid(),
537 initiator.casep.local_sessid(),
538 peer_addr,
539 SessionMode::Case {
540 fab_idx,
541 cat_ids: peer_catids,
542 },
543 Some(dec_key),
544 Some(enc_key),
545 Some(att_challenge),
546 Some(initiator.casep.shared_secret()),
547 )?;
548 }
549
550 session.complete();
551
552 exchange.acknowledge().await?;
553
554 #[cfg(feature = "case-resumption")]
560 {
561 exchange.with_state(|state| {
562 state.resumption.insert_or_update(ResumableSession {
563 fab_idx,
564 peer_nodeid: peer_node_id,
565 peer_cat_ids: peer_catids,
566 resumption_id: peer_resumption_id,
567 shared_secret: crate::crypto::CanonPkcSharedSecret::new_from_ref(
568 initiator.casep.shared_secret(),
569 ),
570 });
571 Ok::<_, Error>(())
572 })?;
573 exchange.matter().transport().notify_resumption_dirty();
574 }
575
576 info!(
577 "CASE session established: local_sessid={}, peer_sessid={}",
578 initiator.casep.local_sessid(),
579 initiator.casep.peer_sessid()
580 );
581
582 Ok(())
583 }
584
585 #[cfg(feature = "case-resumption")]
606 #[allow(clippy::too_many_arguments)]
607 async fn finalize_sigma2_resume(
608 exchange: &mut Exchange<'_>,
609 crypto: &'a C,
610 mut session: ReservedSession<'_>,
611 fab_idx: NonZeroU8,
612 local_sessid: u16,
613 record: ResumableSession,
614 initiator_random: &CaseRandom,
615 ) -> Result<(), Error> {
616 let (new_rid, resume2_mic, peer_sessid) = {
623 let payload = exchange.rx()?.payload();
624 let msg = Sigma2ResumeMsg::from_tlv(&get_root_node_struct(payload)?)?;
625
626 if msg.resumption_id.0.len() != CASE_RESUMPTION_ID_LEN
627 || msg.sigma2_resume_mic.0.len() != AEAD_TAG_LEN
628 {
629 error!(
630 "Sigma2_Resume: bad field length \
631 (resumption_id={}, sigma2_resume_mic={})",
632 msg.resumption_id.0.len(),
633 msg.sigma2_resume_mic.0.len()
634 );
635 complete_with_status(exchange, SCStatusCodes::InvalidParameter, &[]).await?;
636 return Err(ErrorCode::Invalid.into());
637 }
638
639 let rid_bytes: &[u8; CASE_RESUMPTION_ID_LEN] = msg
640 .resumption_id
641 .0
642 .try_into()
643 .map_err(|_| ErrorCode::InvalidData)?;
644 let mut new_rid = CASE_RESUMPTION_ID_ZEROED;
645 new_rid.load_from_array(rid_bytes);
646
647 let mut mic = [0u8; AEAD_TAG_LEN];
648 mic.copy_from_slice(msg.sigma2_resume_mic.0);
649
650 (new_rid, mic, msg.responder_sessid)
651 };
652
653 let mut s2rk = CanonAeadKey::new();
655 derive_resume_key(
656 crypto,
657 ResumeKeyKind::S2rk,
658 record.shared_secret.reference(),
659 initiator_random.reference(),
660 new_rid.reference(),
661 &mut s2rk,
662 )?;
663
664 if verify_resume_mic(crypto, s2rk.reference(), RESUME2_MIC_NONCE, &resume2_mic).is_err() {
665 error!("Sigma2_Resume: Resume2MIC verify failed");
666 complete_with_status(exchange, SCStatusCodes::InvalidParameter, &[]).await?;
667 return Err(ErrorCode::Invalid.into());
668 }
669
670 let mut session_keys = MaybeUninit::<CaseSessionKeys>::uninit();
672 let session_keys = session_keys.init_with(CaseSessionKeys::init());
673 compute_resumption_session_keys(
676 crypto,
677 record.shared_secret.reference(),
678 initiator_random.reference(),
679 record.resumption_id.reference(),
680 session_keys,
681 )?;
682
683 let (enc_key, remaining) = session_keys
686 .reference()
687 .split::<AEAD_CANON_KEY_LEN, { AEAD_CANON_KEY_LEN * 2 }>();
688 let (dec_key, att_challenge) = remaining.split::<AEAD_CANON_KEY_LEN, AEAD_CANON_KEY_LEN>();
689
690 let (peer_addr, local_nodeid) = exchange.with_state(|state| {
692 let sess = exchange.id().session(&mut state.sessions);
693 let fabric = state.fabrics.fabric(fab_idx)?;
694 Ok((sess.get_peer_addr(), fabric.node_id()))
695 })?;
696
697 session.update(
698 local_nodeid,
699 record.peer_nodeid,
700 peer_sessid,
701 local_sessid,
702 peer_addr,
703 SessionMode::Case {
704 fab_idx: record.fab_idx,
705 cat_ids: record.peer_cat_ids,
706 },
707 Some(dec_key),
708 Some(enc_key),
709 Some(att_challenge),
710 Some(record.shared_secret.reference()),
711 )?;
712
713 session.complete();
719
720 complete_with_status(exchange, SCStatusCodes::SessionEstablishmentSuccess, &[]).await?;
722
723 exchange.with_state(|state| {
729 state.resumption.insert_or_update(ResumableSession {
730 fab_idx: record.fab_idx,
731 peer_nodeid: record.peer_nodeid,
732 peer_cat_ids: record.peer_cat_ids,
733 resumption_id: new_rid,
734 shared_secret: record.shared_secret.clone(),
735 });
736 Ok::<_, Error>(())
737 })?;
738 exchange.matter().transport().notify_resumption_dirty();
739
740 info!(
741 "CASE session resumed (initiator): local_sessid={}, peer_sessid={}, \
742 fabric={}, peer_nodeid=0x{:x}",
743 local_sessid,
744 peer_sessid,
745 record.fab_idx.get(),
746 record.peer_nodeid,
747 );
748
749 Ok(())
750 }
751}