Skip to main content

rs_matter/sc/case/
initiator.rs

1/*
2 *
3 *    Copyright (c) 2026 Project CHIP Authors
4 *
5 *    Licensed under the Apache License, Version 2.0 (the "License");
6 *    you may not use this file except in compliance with the License.
7 *    You may obtain a copy of the License at
8 *
9 *        http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *    Unless required by applicable law or agreed to in writing, software
12 *    distributed under the License is distributed on an "AS IS" BASIS,
13 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *    See the License for the specific language governing permissions and
15 *    limitations under the License.
16 */
17
18//! CASE Initiator (Controller side) implementation.
19//!
20//! This module implements the initiator side of the CASE (Certificate Authenticated Session
21//! Establishment) protocol, used by controllers to establish secure sessions with Matter devices.
22
23use 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/// Sigma2 Response structure, parsed from the responder's Sigma2 message.
56#[derive(FromTLV, Debug)]
57#[cfg_attr(feature = "defmt", derive(defmt::Format))]
58#[tlvargs(start = 1, lifetime = "'a")]
59struct Sigma2Resp<'a> {
60    /// The responder's random value
61    responder_random: OctetStr<'a>,
62    /// The responder's session ID
63    responder_sessid: u16,
64    /// The responder's ephemeral public key
65    responder_eph_pub_key: OctetStr<'a>,
66    /// The encrypted TBE2 payload
67    encrypted2: OctetStr<'a>,
68}
69
70/// Decrypted TBE data from Sigma2
71#[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/// Sigma2_Resume response, parsed from the responder's message when it
81/// accepts a Sigma1-with-Resumption.
82#[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    /// The new `ResumptionID` minted by the responder.
88    resumption_id: OctetStr<'a>,
89    /// The 16-byte AEAD tag over empty plaintext with `S2RK`.
90    sigma2_resume_mic: OctetStr<'a>,
91    /// The responder's session ID.
92    responder_sessid: u16,
93    /// Optional responder MRP session parameters. Currently ignored
94    /// by rs-matter's initiator side (parity with the non-resumption
95    /// Sigma2 path).
96    _session_parameters: Option<crate::sc::SessionParameters>,
97}
98
99/// CASE Initiator for establishing secure sessions with Matter devices using operational
100/// certificates.
101///
102/// This implements the controller side of the CASE protocol.
103/// The typical flow is:
104///
105/// 1. Create an exchange to the target device over a plaintext session
106/// 2. Call `CaseInitiator::initiate()` with the fabric index and peer node ID
107/// 3. On success, the exchange's session is upgraded to a secure CASE session
108pub struct CaseInitiator<'a, C: Crypto + 'a> {
109    casep: CaseP<'a, C>,
110    /// The peer's node ID (used to verify responder NOC in process_sigma2)
111    peer_node_id: u64,
112    /// Our ephemeral secret key (retained from start_initiator for ECDH in process_sigma2)
113    secret_key: Option<C::SecretKey<'a>>,
114}
115
116impl<'a, C: Crypto + 'a> CaseInitiator<'a, C> {
117    /// Create a new CASE initiator
118    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    /// Perform a CASE handshake with a Matter device.
127    ///
128    /// This performs the complete CASE handshake:
129    /// 1. Send Sigma1 (initiator_random, session_id, destination_id, eph_pub_key)
130    /// 2. Receive Sigma2 (responder_random, session_id, eph_pub_key, encrypted TBE2)
131    /// 3. Send Sigma3 (encrypted TBE3)
132    /// 4. Receive StatusReport
133    ///
134    /// On success, a new secure CASE session is established with the target device.
135    ///
136    /// # Arguments
137    /// - `exchange` - An exchange to the target device over a plaintext session
138    /// - `crypto` - The crypto implementation
139    /// - `fab_idx` - The fabric index to use for the handshake
140    /// - `peer_node_id` - The node ID of the target device
141    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        // Step 1: Reserve a session slot
148        let mut session = ReservedSession::reserve(exchange.matter(), crypto).await?;
149
150        let mut initiator = Self::new(peer_node_id);
151
152        // Step 1a: Look up any cached CASE session resumption record for
153        // this peer. If present, we'll ask the responder for resumption
154        // by populating tags 6 and 7 on Sigma1; the responder either
155        // returns Sigma2_Resume (accepted) or Sigma2 (fell back to the
156        // full handshake) — both are handled below.
157        //
158        // Without the `case-resumption` feature there is no cache, so we
159        // never offer resumption and always run the full handshake.
160        #[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        // Step 2: Prepare Sigma1 parameters
175        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        // Step 2a: If we have a cached resumption record, precompute
195        // `Resume1MIC` so it can be spliced into Sigma1 below.
196        // `initiator_random` is available now (produced by
197        // `start_initiator`), so we can already derive `S1RK`.
198        //
199        // Without `case-resumption` these are always `(None, None)`, so the
200        // Sigma1 built below carries no resumption fields.
201        #[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        // Step 3: Build and send Sigma1
233        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                // Sigma1 with Resumption: attach the cached `resumptionID`
243                // (tag 6) and the freshly-computed `Resume1MIC` (tag 7).
244                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        // Step 4: Receive Sigma2, Sigma2_Resume, or an error StatusReport
263        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        // We only ever offer resumption when `case-resumption` is on, so a
279        // spec-compliant responder never sends Sigma2_Resume otherwise; the
280        // branch (and its `finalize_sigma2_resume`) is compiled out.
281        #[cfg(feature = "case-resumption")]
282        if response_opcode == OpCode::CASESigma2Resume as u8 {
283            // Responder accepted our resumption request. Complete it
284            // via `finalize_sigma2_resume`. If we didn't request
285            // resumption, this response is a spec violation and we
286            // reject with `InvalidParameter`.
287            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        // Step 5: Decrypt Sigma2 TBE and validate
314        // `peer_resumption_id` (the responder's ResumptionID from TBEData2,
315        // spec-mandated in full CASE) is only consumed to seed the resumption
316        // cache, so it is unused when `case-resumption` is off.
317        #[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                // Copy encrypted2 to a mutable stack buffer for in-place decryption
326                let mut encrypted2_buf = alloc!([0u8; CASE_LARGE_BUF_SIZE]); // TODO LARGE BUFFER
327
328                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                // Decrypt TBE2 (symmetric with sigma3_decrypt on the responder side)
349                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                // Clear the secret key after ECDH
366                initiator.secret_key = None;
367
368                let decrypted = &encrypted2[..len];
369                let decrypted_data = TBEData2Decrypt::from_tlv(&get_root_node_struct(decrypted)?)?;
370
371                // Validate certificate chain
372                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]); // TODO LARGE BUFFER
379                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                // Verify the responder's node ID matches the expected peer
394                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                // Verify signature
405                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                // Extract CAT IDs
420                let mut peer_catids: NocCatIds = Default::default();
421                responder_noc.get_cat_ids(&mut peer_catids)?;
422
423                // Capture resumption ID
424                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        // Step 6: Compute Sigma3 signature (needs fabric borrow, must drop before await)
440        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            // Use a temporary buffer for the TBS data
447            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        // Step 7: Build and send Sigma3
454        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        // Step 8: Receive StatusReport
479        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        // Step 9: Derive session keys and complete the session
508        {
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            // For initiator: first key = I2R (enc_key), second = R2I (dec_key)
527            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        // Seed the resumption cache with this freshly-established full
555        // CASE session so a subsequent handshake with the same peer can
556        // attempt resumption. The `resumption_id` came from `TBEData2`
557        // in Sigma2 (`peer_resumption_id`); `SharedSecret`, peer id and
558        // peer CATs are what we just committed to the `Session`.
559        #[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    /// Complete a CASE resumption from the initiator side, given that
586    /// we have just received a `Sigma2_Resume` in response to a Sigma1
587    /// that carried the resumption fields.
588    ///
589    /// On success:
590    /// - Derives the resumption session keys.
591    /// - Populates the reserved session (rotating the `SharedSecret`
592    ///   into place along with the new keys).
593    /// - Marks the session live via `session.complete()`.
594    /// - Sends `SigmaFinished` (a StatusReport with
595    ///   `SessionEstablishmentSuccess`) which piggybacks the MRP ack
596    ///   for `Sigma2_Resume`, concluding the exchange.
597    /// - Rotates the cache entry: same peer, same `SharedSecret`, new
598    ///   `resumption_id`.
599    ///
600    /// On `Resume2MIC` verification failure the initiator sends
601    /// `InvalidParameter` per Matter spec and returns an error; the
602    /// reserved session is dropped uncomitted.
603    ///
604    /// Compiled only with the `case-resumption` feature.
605    #[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        // ---- Parse Sigma2_Resume, copy out fields. ---------------------
617        //
618        // Same borrow-scoping pattern as the responder path: the parsed
619        // message borrows from the RX buffer, so we copy the small
620        // pieces out into stack storage and let the borrow drop before
621        // we send the SigmaFinished status report.
622        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        // ---- Derive S2RK and verify Resume2MIC. -----------------------
654        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        // ---- Derive resumption session keys. --------------------------
671        let mut session_keys = MaybeUninit::<CaseSessionKeys>::uninit();
672        let session_keys = session_keys.init_with(CaseSessionKeys::init());
673        // Derive session traffic keys from the resumption ID we sent in
674        // Sigma1 (the current ID), not from Sigma2_Resume's rotated ID.
675        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        // As initiator: enc_key = I2R, dec_key = R2I (mirror of the
684        // Sigma3 path — see the fall-through branch above).
685        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        // ---- Populate the reserved session with new keys + secret. ----
691        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        // Mark the session live *before* sending SigmaFinished so that
714        // if the responder immediately reuses the session for an
715        // application message, our receive path can already route it
716        // (the responder considers the session live as soon as it
717        // receives SigmaFinished).
718        session.complete();
719
720        // ---- Send SigmaFinished (piggybacks MRP ack for Sigma2_Resume).
721        complete_with_status(exchange, SCStatusCodes::SessionEstablishmentSuccess, &[]).await?;
722
723        // ---- Rotate the cache entry. ----------------------------------
724        //
725        // `SharedSecret` and peer identity are unchanged; only
726        // `resumption_id` rotates. `insert_or_update` refreshes the
727        // existing record for this peer and moves it to the tail (MRU).
728        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}