Skip to main content

rs_matter/sc/case/
responder.rs

1/*
2 *
3 *    Copyright (c) 2022-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
18use core::{mem::MaybeUninit, num::NonZeroU8};
19
20#[cfg(feature = "case-resumption")]
21use rand_core::RngCore;
22
23#[cfg(feature = "case-resumption")]
24use super::casep::{
25    compute_resume_mic, compute_resumption_session_keys, derive_resume_key, verify_resume_mic,
26    ResumeKeyKind, CASE_RANDOM_LEN, CASE_RESUMPTION_ID_LEN, RESUME1_MIC_NONCE, RESUME2_MIC_NONCE,
27};
28use super::casep::{CaseP, CaseRandom, CaseResumptionId, CaseSessionKeys};
29#[cfg(feature = "case-resumption")]
30use super::resumption::ResumableSession;
31use super::CASE_LARGE_BUF_SIZE;
32use crate::alloc;
33use crate::cert::CertRef;
34#[cfg(feature = "case-resumption")]
35use crate::crypto::{CanonAeadKey, AEAD_TAG_LEN};
36use crate::crypto::{CanonPkcSignature, CanonPkcSignatureRef, Crypto, Hash, AEAD_CANON_KEY_LEN};
37use crate::error::Error;
38#[cfg(feature = "case-resumption")]
39use crate::error::ErrorCode;
40use crate::sc::{
41    check_opcode, complete_with_status, expect_opcode, sc_write, OpCode, SCStatusCodes,
42    SessionParameters,
43};
44#[cfg(feature = "case-resumption")]
45use crate::sc::{GeneralCode, StatusReport};
46use crate::tlv::{get_root_node_struct, FromTLV, OctetStr, TLVElement, TLVTag, TLVWrite, ToTLV};
47use crate::transport::exchange::Exchange;
48use crate::transport::session::{NocCatIds, ReservedSession, SessionMode};
49use crate::utils::init::{init, Init, InitMaybeUninit};
50#[cfg(feature = "case-resumption")]
51use crate::utils::storage::ReadBuf;
52
53/// Sigma1 Request structure
54#[derive(FromTLV, Debug)]
55#[cfg_attr(feature = "defmt", derive(defmt::Format))]
56#[tlvargs(start = 1, lifetime = "'a")]
57struct Sigma1Req<'a> {
58    /// The initiator's random value
59    initiator_random: OctetStr<'a>,
60    /// The initiator's session ID
61    initiator_sessid: u16,
62    /// The destination ID
63    dest_id: OctetStr<'a>,
64    /// The peer's public key
65    peer_pub_key: OctetStr<'a>,
66    /// Session parameters (optional)
67    session_parameters: Option<SessionParameters>,
68    /// Resumption ID (optional)
69    resumption_id: Option<OctetStr<'a>>,
70    /// Initiator Resume MIC (optional)
71    initiator_resume_mic: Option<OctetStr<'a>>,
72}
73
74/// Sigma3 Decrypt structure
75#[derive(FromTLV, Debug)]
76#[cfg_attr(feature = "defmt", derive(defmt::Format))]
77#[tlvargs(start = 1, lifetime = "'a")]
78struct Sigma3Decrypt<'a> {
79    /// The initiator's Node Operational Certificate
80    initiator_noc: OctetStr<'a>,
81    /// The initiator's Intermediate Certificate Authority Certificate (optional)
82    initiator_icac: Option<OctetStr<'a>>,
83    /// The signature
84    signature: OctetStr<'a>,
85}
86
87/// The CASE Responder (device side) handler
88pub struct CaseResponder<'a, C: Crypto> {
89    crypto: &'a C,
90    /// The CASE session state
91    casep: CaseP<'a, C>,
92}
93
94impl<'a, C: Crypto> CaseResponder<'a, C> {
95    /// Create a new `CaseResponder` instance
96    #[inline(always)]
97    pub const fn new(crypto: &'a C) -> Self {
98        Self {
99            crypto,
100            casep: CaseP::new(),
101        }
102    }
103
104    /// Return an in-place initializer for `CaseResponder`
105    pub fn init(crypto: &'a C) -> impl Init<Self> {
106        init!(Self {
107            crypto,
108            casep <- CaseP::init(),
109        })
110    }
111
112    /// Handle the CASE protocol exchange, where the other peer is the exchange initiator.
113    ///
114    /// Consumes the exchange: on return the CASE handshake has either
115    /// completed, been rejected, or aborted, and the exchange is dropped.
116    pub async fn handle(&mut self, mut exchange: Exchange<'_>) -> Result<(), Error> {
117        let mut session = ReservedSession::reserve(exchange.matter(), self.crypto).await?;
118
119        // Attempt session resumption first. If the peer's Sigma1 carries
120        // both `resumptionID` and `initiatorResumeMIC`, we have a cached
121        // record for that resumption id, and the MIC checks out, this
122        // shortcuts to Sigma2_Resume + SigmaFinished. Any other case
123        // (fields absent, unknown id, MIC mismatch) falls through to the
124        // full Sigma1/2/3 flow.
125        //
126        // Without the `case-resumption` feature there is no cache and this
127        // is skipped entirely: every Sigma1 runs the full handshake, which
128        // is spec-compliant (resumption is optional) — a peer that offered
129        // resumption fields simply gets a full Sigma2 back.
130        #[cfg(feature = "case-resumption")]
131        if self
132            .try_handle_sigma1_resume(&mut exchange, &mut session)
133            .await?
134        {
135            return Ok(());
136        }
137
138        self.handle_casesigma1(&mut exchange, &mut session).await?;
139
140        exchange.recv_fetch().await?;
141
142        self.handle_casesigma3(&mut exchange, session).await?;
143
144        exchange.acknowledge().await?;
145
146        Ok(())
147    }
148
149    /// Handle the CASE Sigma1 message
150    ///
151    /// # Arguments
152    /// - `exchange` - The exchange to handle the CASE Sigma1 message on
153    /// - `session` - The reserved CASE session slot that receives the
154    ///   peer's MRP `session_parameters` from Sigma1 so they're in place
155    ///   before it transitions to the established CASE session.
156    async fn handle_casesigma1(
157        &mut self,
158        exchange: &mut Exchange<'_>,
159        session: &mut ReservedSession<'_>,
160    ) -> Result<(), Error> {
161        check_opcode(exchange, OpCode::CASESigma1)?;
162
163        let req = Sigma1Req::from_tlv(&get_root_node_struct(exchange.rx()?.payload())?)?;
164
165        // Per Matter spec, `resumptionID` and `initiatorResumeMIC` must
166        // either both be present or both be absent. A mismatched pair
167        // is malformed and the responder rejects it with
168        // `INVALID_PARAMETER`.
169        if req.resumption_id.is_some() != req.initiator_resume_mic.is_some() {
170            error!("Sigma1 has mismatched resumptionID/initiatorResumeMIC presence; rejecting");
171            complete_with_status(exchange, SCStatusCodes::InvalidParameter, &[]).await?;
172
173            return Ok(());
174        }
175
176        let local_fabric_idx = exchange.with_state(|state| {
177            Ok(state
178                .fabrics
179                .get_by_dest_id(self.crypto, req.initiator_random.0, req.dest_id.0)
180                .map(|fabric| fabric.fab_idx()))
181        })?;
182
183        if local_fabric_idx.is_none() {
184            error!("Fabric Index mismatch");
185            complete_with_status(exchange, SCStatusCodes::NoSharedTrustRoots, &[]).await?;
186
187            return Ok(());
188        }
189
190        let local_sessid = exchange.with_state(|state| Ok(state.sessions.get_next_sess_id()))?;
191
192        let mut our_random = MaybeUninit::<CaseRandom>::uninit(); // TODO MEDIUM BUFFER
193        let our_random = our_random.init_with(CaseRandom::init());
194
195        let mut resumption_id = MaybeUninit::<CaseResumptionId>::uninit(); // TODO MEDIUM BUFFER
196        let resumption_id = resumption_id.init_with(CaseResumptionId::init());
197
198        let mut tt_hash = MaybeUninit::<Hash>::uninit(); // TODO MEDIUM BUFFER
199        let tt_hash = tt_hash.init_with(Hash::init());
200
201        self.casep.start(
202            self.crypto,
203            req.initiator_sessid,
204            local_sessid,
205            unwrap!(local_fabric_idx).get(),
206            req.peer_pub_key.0.try_into()?,
207            exchange.rx()?.payload(),
208            our_random,
209            resumption_id,
210            tt_hash,
211        )?;
212
213        // Stash the initiator's advertised MRP `session_parameters`
214        // so the responder uses the peer's SAI as the retransmission
215        // base interval for Sigma2 and any post-handshake traffic. We
216        // apply them both to the unsecured session that the handshake
217        // currently rides on (so Sigma2 retransmits use them) and to
218        // the reserved CASE session that takes over after Sigma3.
219        if let Some(params) = req.session_parameters.as_ref() {
220            exchange.with_state(|state| {
221                exchange
222                    .id()
223                    .session(&mut state.sessions)
224                    .set_peer_session_params(params);
225                Ok(())
226            })?;
227            session.set_peer_session_params(params)?;
228        }
229
230        trace!(
231            "Destination ID matched to fabric index {}",
232            self.casep.local_fabric_idx()
233        );
234
235        // `send_with` can call its closure again for an MRP retransmission.
236        // ECDSA signing may be randomized, so generate the signature only once
237        // to keep every Sigma2 byte-identical and avoid reusing the Sigma2 AEAD
238        // nonce with different plaintext.
239        let mut signature = MaybeUninit::<CanonPkcSignature>::uninit(); // TODO MEDIUM BUFFER
240        let signature = signature.init_with(CanonPkcSignature::init());
241        let mut signature_generated = false;
242        let mut tt_updated = false;
243        exchange
244            .send_with(|exchange, tw| {
245                exchange.with_state(|state| {
246                    let fabric = NonZeroU8::new(self.casep.local_fabric_idx())
247                        .and_then(|fabric_idx| state.fabrics.get(fabric_idx));
248
249                    let Some(fabric) = fabric else {
250                        return sc_write(tw, SCStatusCodes::NoSharedTrustRoots, &[]);
251                    };
252
253                    if !signature_generated {
254                        // Use the remainder of the TX buffer as scratch space for computing the
255                        // signature.
256                        let sign_buf = tw.empty_as_mut_slice();
257
258                        self.casep.compute_sigma2_signature(
259                            self.crypto,
260                            fabric,
261                            sign_buf,
262                            signature,
263                        )?;
264                        signature_generated = true;
265                    }
266
267                    tw.start_struct(&TLVTag::Anonymous)?;
268                    tw.str(&TLVTag::Context(1), our_random.access())?;
269                    tw.u16(&TLVTag::Context(2), local_sessid)?;
270                    tw.str(&TLVTag::Context(3), self.casep.our_pub_key().access())?;
271
272                    tw.str_cb(&TLVTag::Context(4), |buf| {
273                        self.casep.sigma2_encrypt(
274                            self.crypto,
275                            fabric,
276                            our_random.reference(),
277                            tt_hash.reference(),
278                            signature.reference(),
279                            resumption_id.reference(),
280                            buf,
281                        )
282                    })?;
283
284                    // Responder session parameters (tag 5)
285                    let session_params = crate::sc::SessionParameters {
286                        max_paths_per_invoke: Some(
287                            exchange.matter().dev_det().max_paths_per_invoke,
288                        ),
289                        ..Default::default()
290                    };
291                    session_params.to_tlv(&TLVTag::Context(5), &mut *tw)?;
292
293                    tw.end_container()?;
294
295                    if !tt_updated {
296                        self.casep.update_tt(tw.as_slice())?;
297                        tt_updated = true;
298                    }
299
300                    Ok(Some(OpCode::CASESigma2.into()))
301                })
302            })
303            .await
304    }
305
306    /// Handle the CASE Sigma3 message
307    ///
308    /// # Arguments
309    /// - `exchange` - The exchange to handle the CASE Sigma3 message on
310    /// - `session` - The reserved session to complete upon successful CASE handshake
311    async fn handle_casesigma3(
312        &mut self,
313        exchange: &mut Exchange<'_>,
314        mut session: ReservedSession<'_>,
315    ) -> Result<(), Error> {
316        expect_opcode(exchange, OpCode::CASESigma3).await?;
317
318        let status = exchange.with_state(|state| {
319            let sess = exchange.id().session(&mut state.sessions);
320
321            let fabric = NonZeroU8::new(self.casep.local_fabric_idx())
322                .and_then(|fabric_idx| state.fabrics.get(fabric_idx));
323            if let Some(fabric) = fabric {
324                // A malformed or corrupted Sigma3 — bad TLV at the outer
325                // wrapper, an oversized `TBEData3Encrypted` field, AEAD auth
326                // failure, or a decrypted payload that doesn't parse — must
327                // be reported back to the peer with `INVALID_PARAMETER`
328                // rather than silently abandoning the exchange (TC-SC-3.4
329                // step 5 covers this).
330                let req = match get_root_node_struct(exchange.rx()?.payload()) {
331                    Ok(req) => req,
332                    Err(e) => {
333                        error!("Sigma3 outer TLV parse failed: {}", e);
334                        return Ok(SCStatusCodes::InvalidParameter);
335                    }
336                };
337                let encrypted = match req.structure().and_then(|s| s.ctx(1)).and_then(|c| c.str()) {
338                    Ok(s) => s,
339                    Err(e) => {
340                        error!("Sigma3 encrypted field parse failed: {}", e);
341                        return Ok(SCStatusCodes::InvalidParameter);
342                    }
343                };
344
345                let mut decrypted = alloc!([0; CASE_LARGE_BUF_SIZE]); // TODO LARGE BUFFER
346                if encrypted.len() > decrypted.len() {
347                    error!(
348                        "Encrypted Sigma3 data too large ({} bytes)",
349                        encrypted.len()
350                    );
351                    return Ok(SCStatusCodes::InvalidParameter);
352                }
353
354                let decrypted = &mut decrypted[..encrypted.len()];
355                decrypted.copy_from_slice(encrypted);
356
357                let len =
358                    match self
359                        .casep
360                        .sigma3_decrypt(self.crypto, fabric.ipk().op_key(), decrypted)
361                    {
362                        Ok(len) => len,
363                        Err(e) => {
364                            error!("Sigma3 AEAD decrypt failed: {}", e);
365                            return Ok(SCStatusCodes::InvalidParameter);
366                        }
367                    };
368                let decrypted = &decrypted[..len];
369                let decrypted_req: Sigma3Decrypt<'_> = match get_root_node_struct(decrypted)
370                    .and_then(|n| Sigma3Decrypt::from_tlv(&n))
371                {
372                    Ok(req) => req,
373                    Err(e) => {
374                        error!("Sigma3 decrypted TLV parse failed: {}", e);
375                        return Ok(SCStatusCodes::InvalidParameter);
376                    }
377                };
378
379                let initiator_noc = CertRef::new(TLVElement::new(decrypted_req.initiator_noc.0));
380                let initiator_icac = decrypted_req
381                    .initiator_icac
382                    .map(|icac| CertRef::new(TLVElement::new(icac.0)));
383
384                let mut buf = alloc!([0; CASE_LARGE_BUF_SIZE]); // TODO LARGE BUFFER
385                let buf = &mut buf[..];
386                if let Err(e) = self.casep.validate_certs(
387                    self.crypto,
388                    state.rtc.utc_time(),
389                    fabric,
390                    &initiator_noc,
391                    initiator_icac.as_ref(),
392                    buf,
393                ) {
394                    error!("Certificate Chain doesn't match: {}", e);
395                    Ok(SCStatusCodes::InvalidParameter)
396                } else if let Err(e) = self.casep.validate_peer_tbs_signature(
397                    self.crypto,
398                    decrypted_req.initiator_noc.0,
399                    decrypted_req.initiator_icac.map(|a| a.0),
400                    &initiator_noc,
401                    CanonPkcSignatureRef::try_new(decrypted_req.signature.0)?,
402                    buf,
403                ) {
404                    error!("Sigma3 Signature doesn't match: {}", e);
405                    Ok(SCStatusCodes::InvalidParameter)
406                } else {
407                    // Only now do we add this message to the TT Hash
408                    let mut peer_catids: NocCatIds = Default::default();
409                    initiator_noc.get_cat_ids(&mut peer_catids)?;
410                    self.casep.update_tt(exchange.rx()?.payload())?;
411
412                    let mut session_keys = MaybeUninit::<CaseSessionKeys>::uninit(); // TODO MEDIM BUFFER
413                    let session_keys = session_keys.init_with(CaseSessionKeys::init());
414                    self.casep.compute_session_keys(
415                        self.crypto,
416                        fabric.ipk().op_key(),
417                        session_keys,
418                    )?;
419
420                    let peer_addr = sess.get_peer_addr();
421
422                    let (dec_key, remaining) = session_keys
423                        .reference()
424                        .split::<AEAD_CANON_KEY_LEN, { AEAD_CANON_KEY_LEN * 2 }>();
425                    let (enc_key, att_challenge) =
426                        remaining.split::<AEAD_CANON_KEY_LEN, AEAD_CANON_KEY_LEN>();
427
428                    session.update_with_state(
429                        state,
430                        fabric.node_id(),
431                        initiator_noc.get_node_id()?,
432                        self.casep.peer_sessid(),
433                        self.casep.local_sessid(),
434                        peer_addr,
435                        SessionMode::Case {
436                            // Unwrapping is safe, because if the fabric index was 0, we would not be in here
437                            fab_idx: unwrap!(NonZeroU8::new(self.casep.local_fabric_idx())),
438                            cat_ids: peer_catids,
439                        },
440                        Some(dec_key),
441                        Some(enc_key),
442                        Some(att_challenge),
443                        Some(self.casep.shared_secret()),
444                    )?;
445
446                    // Seed the resumption cache with this freshly-established
447                    // full CASE session so a subsequent handshake with the
448                    // same peer can attempt resumption. `SharedSecret`,
449                    // fab_idx, peer_nodeid and peer CATs are the same as
450                    // what we just loaded into the `Session`; the
451                    // `resumption_id` was minted by the responder in
452                    // `casep.start` and is still held on `self.casep`.
453                    #[cfg(feature = "case-resumption")]
454                    state.resumption.insert_or_update(ResumableSession {
455                        // Unwrap is safe: we are inside the `fabric` branch.
456                        fab_idx: unwrap!(NonZeroU8::new(self.casep.local_fabric_idx())),
457                        peer_nodeid: initiator_noc.get_node_id()?,
458                        peer_cat_ids: peer_catids,
459                        resumption_id: super::casep::CaseResumptionId::new_from_ref(
460                            self.casep.resumption_id(),
461                        ),
462                        shared_secret: crate::crypto::CanonPkcSharedSecret::new_from_ref(
463                            self.casep.shared_secret(),
464                        ),
465                    });
466
467                    Ok(SCStatusCodes::SessionEstablishmentSuccess)
468                }
469            } else {
470                Ok(SCStatusCodes::NoSharedTrustRoots)
471            }
472        })?;
473
474        if matches!(status, SCStatusCodes::SessionEstablishmentSuccess) {
475            // Complete the reserved session and thus make the `Session` instance
476            // immediately available for use by the system.
477            //
478            // We need to do this _before_ we send the response to the peer, or else we risk missing
479            // (dropping) the first messages the peer would send us on the newly-established session,
480            // as it might start using it right after it receives the response, while it is still marked
481            // as reserved.
482            session.complete();
483
484            // The `state.resumption.insert_or_update` call above dirties
485            // the cache; wake the background persist task.
486            #[cfg(feature = "case-resumption")]
487            exchange.matter().transport().notify_resumption_dirty();
488        }
489
490        complete_with_status(exchange, status, &[]).await
491    }
492
493    /// Try to handle the received Sigma1 as a session-resumption
494    /// request. Returns:
495    ///
496    /// (Compiled only with the `case-resumption` feature; the caller skips
497    /// the resume attempt entirely when it is off.)
498    ///
499    /// - `Ok(true)` — the resumption path fully handled the exchange
500    ///   (whether successfully or with the initiator rejecting the
501    ///   resumption). The caller must not fall through to the normal
502    ///   Sigma1/2/3 flow.
503    /// - `Ok(false)` — the Sigma1 does not carry resumption fields, or
504    ///   we could not honour the resumption (unknown `resumptionID`,
505    ///   MIC verify failed, malformed fields). The caller must continue
506    ///   with the full handshake, treating this Sigma1 as if it had no
507    ///   resumption fields (per Matter spec's fall-through rule).
508    /// - `Err(_)` — a hard error unrelated to resumption (I/O,
509    ///   cryptographic backend failure). The exchange is aborted.
510    #[cfg(feature = "case-resumption")]
511    async fn try_handle_sigma1_resume(
512        &mut self,
513        exchange: &mut Exchange<'_>,
514        session: &mut ReservedSession<'_>,
515    ) -> Result<bool, Error> {
516        check_opcode(exchange, OpCode::CASESigma1)?;
517
518        // ---- Parse Sigma1 and copy out everything we need. -------------
519        //
520        // The parsed `Sigma1Req` borrows from the RX buffer, and we later
521        // need mutable access to the exchange (`send_with`) so we cannot
522        // hold that borrow across the send. Copy the small pieces into
523        // owned stack storage and let `req` drop before we send.
524        let (init_random, init_sessid, incoming_rid, incoming_mic, peer_params) = {
525            let payload = exchange.rx()?.payload();
526            let req = Sigma1Req::from_tlv(&get_root_node_struct(payload)?)?;
527
528            // Spec: both `resumptionID` and `initiatorResumeMIC` are
529            // present, or neither is. If only one is present the outer
530            // `handle_casesigma1` will produce INVALID_PARAMETER; we just
531            // decline resumption here.
532            let (Some(rid), Some(mic)) = (
533                req.resumption_id.as_ref(),
534                req.initiator_resume_mic.as_ref(),
535            ) else {
536                return Ok(false);
537            };
538
539            if rid.0.len() != CASE_RESUMPTION_ID_LEN || mic.0.len() != AEAD_TAG_LEN {
540                // Bad shape — let the full-handshake path reject it.
541                return Ok(false);
542            }
543
544            let random_bytes: &[u8; CASE_RANDOM_LEN] = req
545                .initiator_random
546                .0
547                .try_into()
548                .map_err(|_| ErrorCode::InvalidData)?;
549            let mut init_random = CaseRandom::new();
550            init_random.load_from_array(random_bytes);
551
552            let rid_bytes: &[u8; CASE_RESUMPTION_ID_LEN] =
553                rid.0.try_into().map_err(|_| ErrorCode::InvalidData)?;
554            let mut incoming_rid = CaseResumptionId::new();
555            incoming_rid.load_from_array(rid_bytes);
556
557            let mut incoming_mic = [0u8; AEAD_TAG_LEN];
558            incoming_mic.copy_from_slice(mic.0);
559
560            (
561                init_random,
562                req.initiator_sessid,
563                incoming_rid,
564                incoming_mic,
565                req.session_parameters.clone(),
566            )
567        };
568
569        // ---- Look up the cached resumption record. ---------------------
570        let record = exchange.with_state(|state| {
571            Ok::<_, Error>(
572                state
573                    .resumption
574                    .find_by_resumption_id(incoming_rid.reference().access())
575                    .cloned(),
576            )
577        })?;
578
579        let Some(record) = record else {
580            debug!(
581                "CASE Sigma1 resumption: no cached record for the requested resumption id; \
582                 falling back to full handshake"
583            );
584            return Ok(false);
585        };
586
587        // ---- Derive S1RK and verify Resume1MIC. ------------------------
588        let mut s1rk = CanonAeadKey::new();
589        derive_resume_key(
590            self.crypto,
591            ResumeKeyKind::S1rk,
592            record.shared_secret.reference(),
593            init_random.reference(),
594            record.resumption_id.reference(),
595            &mut s1rk,
596        )?;
597
598        if verify_resume_mic(
599            self.crypto,
600            s1rk.reference(),
601            RESUME1_MIC_NONCE,
602            &incoming_mic,
603        )
604        .is_err()
605        {
606            debug!(
607                "CASE Sigma1 resumption: Resume1MIC verify failed for peer node id \
608                 0x{:x} on fabric {}; falling back to full handshake",
609                record.peer_nodeid,
610                record.fab_idx.get()
611            );
612            return Ok(false);
613        }
614
615        // ---- Mint a new resumption id + derive S2RK + Resume2MIC. ------
616        let mut new_rid = CaseResumptionId::new();
617        self.crypto.rand()?.fill_bytes(new_rid.access_mut());
618
619        let mut s2rk = CanonAeadKey::new();
620        derive_resume_key(
621            self.crypto,
622            ResumeKeyKind::S2rk,
623            record.shared_secret.reference(),
624            init_random.reference(),
625            new_rid.reference(),
626            &mut s2rk,
627        )?;
628
629        let mut resume2_mic = [0u8; AEAD_TAG_LEN];
630        compute_resume_mic(
631            self.crypto,
632            s2rk.reference(),
633            RESUME2_MIC_NONCE,
634            &mut resume2_mic,
635        )?;
636
637        // ---- Prepare session context (peer id, IDs, MRP params). -------
638        let local_sessid = exchange.with_state(|state| Ok(state.sessions.get_next_sess_id()))?;
639
640        // Apply peer's Sigma1 MRP `session_parameters` to both the
641        // unsecured session that carries the handshake (so Sigma2_Resume
642        // retransmits use them) and to the reserved session that takes
643        // over after SigmaFinished. Mirrors the equivalent block in
644        // `handle_casesigma1`.
645        if let Some(ref params) = peer_params {
646            exchange.with_state(|state| {
647                exchange
648                    .id()
649                    .session(&mut state.sessions)
650                    .set_peer_session_params(params);
651                Ok::<_, Error>(())
652            })?;
653            session.set_peer_session_params(params)?;
654        }
655
656        // ---- Send Sigma2_Resume. --------------------------------------
657        let responder_session_params = SessionParameters {
658            max_paths_per_invoke: Some(exchange.matter().dev_det().max_paths_per_invoke),
659            ..Default::default()
660        };
661        let new_rid_bytes: [u8; CASE_RESUMPTION_ID_LEN] = *new_rid.reference().access();
662
663        exchange
664            .send_with(|_, tw| {
665                tw.start_struct(&TLVTag::Anonymous)?;
666                tw.str(&TLVTag::Context(1), &new_rid_bytes)?;
667                tw.str(&TLVTag::Context(2), &resume2_mic)?;
668                tw.u16(&TLVTag::Context(3), local_sessid)?;
669                responder_session_params.to_tlv(&TLVTag::Context(4), &mut *tw)?;
670                tw.end_container()?;
671
672                Ok(Some(OpCode::CASESigma2Resume.into()))
673            })
674            .await?;
675
676        // ---- Derive resumption session keys. --------------------------
677        let mut session_keys = MaybeUninit::<CaseSessionKeys>::uninit();
678        let session_keys = session_keys.init_with(CaseSessionKeys::init());
679        // Derive session traffic keys from the resumption ID carried in
680        // Sigma1 (the current ID), not the rotated `new_rid` from
681        // Sigma2_Resume.
682        compute_resumption_session_keys(
683            self.crypto,
684            record.shared_secret.reference(),
685            init_random.reference(),
686            record.resumption_id.reference(),
687            session_keys,
688        )?;
689
690        // As a responder: dec_key = I2R, enc_key = R2I (mirror of the
691        // Sigma3 path in `handle_casesigma3`).
692        let (dec_key, remaining) = session_keys
693            .reference()
694            .split::<AEAD_CANON_KEY_LEN, { AEAD_CANON_KEY_LEN * 2 }>();
695        let (enc_key, att_challenge) = remaining.split::<AEAD_CANON_KEY_LEN, AEAD_CANON_KEY_LEN>();
696
697        // ---- Load keys + identity into the reserved session. -----------
698        exchange.with_state(|state| {
699            let local_nodeid = state
700                .fabrics
701                .get(record.fab_idx)
702                .map(|f| f.node_id())
703                .ok_or(ErrorCode::Invalid)?;
704            let peer_addr = exchange.id().session(&mut state.sessions).get_peer_addr();
705
706            session.update_with_state(
707                state,
708                local_nodeid,
709                record.peer_nodeid,
710                init_sessid,
711                local_sessid,
712                peer_addr,
713                SessionMode::Case {
714                    fab_idx: record.fab_idx,
715                    cat_ids: record.peer_cat_ids,
716                },
717                Some(dec_key),
718                Some(enc_key),
719                Some(att_challenge),
720                Some(record.shared_secret.reference()),
721            )
722        })?;
723
724        // ---- Wait for SigmaFinished (StatusReport). --------------------
725        exchange.recv_fetch().await?;
726
727        let ok = {
728            let rx = exchange.rx()?;
729            let meta = rx.meta();
730            if meta.proto_opcode != OpCode::StatusReport as u8 {
731                warn!(
732                    "CASE resumption: expected StatusReport after Sigma2_Resume, got {}",
733                    meta.proto_opcode
734                );
735                false
736            } else {
737                let mut rb = ReadBuf::new(rx.payload());
738                match StatusReport::read(&mut rb) {
739                    Ok(status)
740                        if status.general_code == GeneralCode::Success
741                            && status.proto_code
742                                == SCStatusCodes::SessionEstablishmentSuccess as u16 =>
743                    {
744                        true
745                    }
746                    Ok(status) => {
747                        warn!(
748                            "CASE resumption: SigmaFinished failed: general={:?}, proto_code={}",
749                            status.general_code, status.proto_code
750                        );
751                        false
752                    }
753                    Err(e) => {
754                        warn!("CASE resumption: failed to parse SigmaFinished: {}", e);
755                        false
756                    }
757                }
758            }
759        };
760
761        if !ok {
762            // Initiator rejected the resumption (or sent a malformed
763            // SigmaFinished). The reserved session is dropped by
764            // `ReservedSession::drop` since we never call
765            // `session.complete()`. Do not fall through to the full
766            // handshake: this exchange has already produced Sigma2_Resume.
767            exchange.acknowledge().await?;
768            return Ok(true);
769        }
770
771        // Mark the session live *before* acknowledging SigmaFinished, so
772        // that if the initiator races an application-layer message right
773        // after its SigmaFinished, the receive path can already route it
774        // (mirrors the ordering in `handle_casesigma3`).
775        session.complete();
776        exchange.acknowledge().await?;
777
778        // ---- Update the cache with the rotated resumption id. ----------
779        //
780        // `SharedSecret` and peer identity are unchanged; only the
781        // `resumption_id` is rotated. `insert_or_update` refreshes the
782        // existing record for this peer and moves it to the tail (MRU).
783        exchange.with_state(|state| {
784            state.resumption.insert_or_update(ResumableSession {
785                fab_idx: record.fab_idx,
786                peer_nodeid: record.peer_nodeid,
787                peer_cat_ids: record.peer_cat_ids,
788                resumption_id: new_rid,
789                shared_secret: record.shared_secret.clone(),
790            });
791            Ok::<_, Error>(())
792        })?;
793        exchange.matter().transport().notify_resumption_dirty();
794
795        info!(
796            "CASE session resumed: local_sessid={}, peer_sessid={}, fabric={}, peer_nodeid=0x{:x}",
797            local_sessid,
798            init_sessid,
799            record.fab_idx.get(),
800            record.peer_nodeid,
801        );
802
803        Ok(true)
804    }
805}