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
20use super::casep::{CaseP, CaseRandom, CaseResumptionId, CaseSessionKeys};
21use super::CASE_LARGE_BUF_SIZE;
22use crate::alloc;
23use crate::cert::CertRef;
24use crate::crypto::{CanonPkcSignature, CanonPkcSignatureRef, Crypto, Hash, AEAD_CANON_KEY_LEN};
25use crate::error::Error;
26use crate::sc::{
27    check_opcode, complete_with_status, sc_write, OpCode, SCStatusCodes, SessionParameters,
28};
29use crate::tlv::{get_root_node_struct, FromTLV, OctetStr, TLVElement, TLVTag, TLVWrite, ToTLV};
30use crate::transport::exchange::Exchange;
31use crate::transport::session::{NocCatIds, ReservedSession, SessionMode};
32use crate::utils::init::{init, Init, InitMaybeUninit};
33
34/// Sigma1 Request structure
35#[derive(FromTLV, Debug)]
36#[cfg_attr(feature = "defmt", derive(defmt::Format))]
37#[tlvargs(start = 1, lifetime = "'a")]
38struct Sigma1Req<'a> {
39    /// The initiator's random value
40    initiator_random: OctetStr<'a>,
41    /// The initiator's session ID
42    initiator_sessid: u16,
43    /// The destination ID
44    dest_id: OctetStr<'a>,
45    /// The peer's public key
46    peer_pub_key: OctetStr<'a>,
47    /// Session parameters (optional)
48    session_parameters: Option<SessionParameters>,
49    /// Resumption ID (optional)
50    resumption_id: Option<OctetStr<'a>>,
51    /// Initiator Resume MIC (optional)
52    initiator_resume_mic: Option<OctetStr<'a>>,
53}
54
55/// Sigma3 Decrypt structure
56#[derive(FromTLV, Debug)]
57#[cfg_attr(feature = "defmt", derive(defmt::Format))]
58#[tlvargs(start = 1, lifetime = "'a")]
59struct Sigma3Decrypt<'a> {
60    /// The initiator's Node Operational Certificate
61    initiator_noc: OctetStr<'a>,
62    /// The initiator's Intermediate Certificate Authority Certificate (optional)
63    initiator_icac: Option<OctetStr<'a>>,
64    /// The signature
65    signature: OctetStr<'a>,
66}
67
68/// The CASE Responder (device side) handler
69pub struct CaseResponder<'a, C: Crypto> {
70    crypto: &'a C,
71    /// The CASE session state
72    casep: CaseP<'a, C>,
73}
74
75impl<'a, C: Crypto> CaseResponder<'a, C> {
76    /// Create a new `CaseResponder` instance
77    #[inline(always)]
78    pub const fn new(crypto: &'a C) -> Self {
79        Self {
80            crypto,
81            casep: CaseP::new(),
82        }
83    }
84
85    /// Return an in-place initializer for `CaseResponder`
86    pub fn init(crypto: &'a C) -> impl Init<Self> {
87        init!(Self {
88            crypto,
89            casep <- CaseP::init(),
90        })
91    }
92
93    /// Handle the CASE protocol exchange, where the other peer is the exchange initiator
94    ///
95    /// # Arguments
96    /// - `exchange` - The exchange to handle the CASE protocol on
97    pub async fn handle(&mut self, exchange: &mut Exchange<'_>) -> Result<(), Error> {
98        let mut session = ReservedSession::reserve(exchange.matter(), self.crypto).await?;
99
100        self.handle_casesigma1(exchange, &mut session).await?;
101
102        exchange.recv_fetch().await?;
103
104        self.handle_casesigma3(exchange, session).await?;
105
106        exchange.acknowledge().await?;
107
108        Ok(())
109    }
110
111    /// Handle the CASE Sigma1 message
112    ///
113    /// # Arguments
114    /// - `exchange` - The exchange to handle the CASE Sigma1 message on
115    /// - `session` - The reserved CASE session slot that receives the
116    ///   peer's MRP `session_parameters` from Sigma1 so they're in place
117    ///   before it transitions to the established CASE session.
118    async fn handle_casesigma1(
119        &mut self,
120        exchange: &mut Exchange<'_>,
121        session: &mut ReservedSession<'_>,
122    ) -> Result<(), Error> {
123        check_opcode(exchange, OpCode::CASESigma1)?;
124
125        let req = Sigma1Req::from_tlv(&get_root_node_struct(exchange.rx()?.payload())?)?;
126
127        // Matter Core spec: `resumptionID` and
128        // `initiatorResumeMIC` SHALL either both be present or both be
129        // absent. A mismatched pair is a malformed Sigma1 and the
130        // responder MUST reject it with `INVALID_PARAMETER` and stop
131        // processing (TC-SC-3.4 steps 1 and 2 cover this).
132        if req.resumption_id.is_some() != req.initiator_resume_mic.is_some() {
133            error!("Sigma1 has mismatched resumptionID/initiatorResumeMIC presence; rejecting");
134            complete_with_status(exchange, SCStatusCodes::InvalidParameter, &[]).await?;
135
136            return Ok(());
137        }
138
139        let local_fabric_idx = exchange.with_state(|state| {
140            Ok(state
141                .fabrics
142                .get_by_dest_id(self.crypto, req.initiator_random.0, req.dest_id.0)
143                .map(|fabric| fabric.fab_idx()))
144        })?;
145
146        if local_fabric_idx.is_none() {
147            error!("Fabric Index mismatch");
148            complete_with_status(exchange, SCStatusCodes::NoSharedTrustRoots, &[]).await?;
149
150            return Ok(());
151        }
152
153        let local_sessid = exchange.with_state(|state| Ok(state.sessions.get_next_sess_id()))?;
154
155        let mut our_random = MaybeUninit::<CaseRandom>::uninit(); // TODO MEDIUM BUFFER
156        let our_random = our_random.init_with(CaseRandom::init());
157
158        let mut resumption_id = MaybeUninit::<CaseResumptionId>::uninit(); // TODO MEDIUM BUFFER
159        let resumption_id = resumption_id.init_with(CaseResumptionId::init());
160
161        let mut tt_hash = MaybeUninit::<Hash>::uninit(); // TODO MEDIUM BUFFER
162        let tt_hash = tt_hash.init_with(Hash::init());
163
164        self.casep.start(
165            self.crypto,
166            req.initiator_sessid,
167            local_sessid,
168            unwrap!(local_fabric_idx).get(),
169            req.peer_pub_key.0.try_into()?,
170            exchange.rx()?.payload(),
171            our_random,
172            resumption_id,
173            tt_hash,
174        )?;
175
176        // Stash the initiator's advertised MRP `session_parameters`
177        // (Matter Core spec) so the responder uses the peer's
178        // SAI as the retransmission base interval for Sigma2 and any
179        // post-handshake traffic. We apply them both to the unsecured
180        // session that the handshake currently rides on (so Sigma2
181        // retransmits use them) and to the reserved CASE session that
182        // takes over after Sigma3.
183        if let Some(params) = req.session_parameters.as_ref() {
184            exchange.with_state(|state| {
185                exchange
186                    .id()
187                    .session(&mut state.sessions)
188                    .set_peer_session_params(params);
189                Ok(())
190            })?;
191            session.set_peer_session_params(params)?;
192        }
193
194        trace!(
195            "Destination ID matched to fabric index {}",
196            self.casep.local_fabric_idx()
197        );
198
199        let mut tt_updated = false;
200        exchange
201            .send_with(|exchange, tw| {
202                exchange.with_state(|state| {
203                    let fabric = NonZeroU8::new(self.casep.local_fabric_idx())
204                        .and_then(|fabric_idx| state.fabrics.get(fabric_idx));
205
206                    let Some(fabric) = fabric else {
207                        return sc_write(tw, SCStatusCodes::NoSharedTrustRoots, &[]);
208                    };
209
210                    let mut signature = MaybeUninit::<CanonPkcSignature>::uninit(); // TODO MEDIUM BUFFER
211                    let signature = signature.init_with(CanonPkcSignature::init());
212
213                    // Use the remainder of the TX buffer as scratch space for computing the signature
214                    let sign_buf = tw.empty_as_mut_slice();
215
216                    self.casep.compute_sigma2_signature(
217                        self.crypto,
218                        fabric,
219                        sign_buf,
220                        signature,
221                    )?;
222
223                    tw.start_struct(&TLVTag::Anonymous)?;
224                    tw.str(&TLVTag::Context(1), our_random.access())?;
225                    tw.u16(&TLVTag::Context(2), local_sessid)?;
226                    tw.str(&TLVTag::Context(3), self.casep.our_pub_key().access())?;
227
228                    tw.str_cb(&TLVTag::Context(4), |buf| {
229                        self.casep.sigma2_encrypt(
230                            self.crypto,
231                            fabric,
232                            our_random.reference(),
233                            tt_hash.reference(),
234                            signature.reference(),
235                            resumption_id.reference(),
236                            buf,
237                        )
238                    })?;
239
240                    // Responder session parameters (tag 5)
241                    let session_params = crate::sc::SessionParameters {
242                        max_paths_per_invoke: Some(
243                            exchange.matter().dev_det().max_paths_per_invoke,
244                        ),
245                        ..Default::default()
246                    };
247                    session_params.to_tlv(&TLVTag::Context(5), &mut *tw)?;
248
249                    tw.end_container()?;
250
251                    if !tt_updated {
252                        self.casep.update_tt(tw.as_slice())?;
253                        tt_updated = true;
254                    }
255
256                    Ok(Some(OpCode::CASESigma2.into()))
257                })
258            })
259            .await
260    }
261
262    /// Handle the CASE Sigma3 message
263    ///
264    /// # Arguments
265    /// - `exchange` - The exchange to handle the CASE Sigma3 message on
266    /// - `session` - The reserved session to complete upon successful CASE handshake
267    async fn handle_casesigma3(
268        &mut self,
269        exchange: &mut Exchange<'_>,
270        mut session: ReservedSession<'_>,
271    ) -> Result<(), Error> {
272        check_opcode(exchange, OpCode::CASESigma3)?;
273
274        let status = exchange.with_state(|state| {
275            let sess = exchange.id().session(&mut state.sessions);
276
277            let fabric = NonZeroU8::new(self.casep.local_fabric_idx())
278                .and_then(|fabric_idx| state.fabrics.get(fabric_idx));
279            if let Some(fabric) = fabric {
280                // A malformed or corrupted Sigma3 — bad TLV at the outer
281                // wrapper, an oversized `TBEData3Encrypted` field, AEAD auth
282                // failure, or a decrypted payload that doesn't parse — must
283                // be reported back to the peer with `INVALID_PARAMETER`
284                // rather than silently abandoning the exchange (TC-SC-3.4
285                // step 5 covers this).
286                let req = match get_root_node_struct(exchange.rx()?.payload()) {
287                    Ok(req) => req,
288                    Err(e) => {
289                        error!("Sigma3 outer TLV parse failed: {}", e);
290                        return Ok(SCStatusCodes::InvalidParameter);
291                    }
292                };
293                let encrypted = match req.structure().and_then(|s| s.ctx(1)).and_then(|c| c.str()) {
294                    Ok(s) => s,
295                    Err(e) => {
296                        error!("Sigma3 encrypted field parse failed: {}", e);
297                        return Ok(SCStatusCodes::InvalidParameter);
298                    }
299                };
300
301                let mut decrypted = alloc!([0; CASE_LARGE_BUF_SIZE]); // TODO LARGE BUFFER
302                if encrypted.len() > decrypted.len() {
303                    error!(
304                        "Encrypted Sigma3 data too large ({} bytes)",
305                        encrypted.len()
306                    );
307                    return Ok(SCStatusCodes::InvalidParameter);
308                }
309
310                let decrypted = &mut decrypted[..encrypted.len()];
311                decrypted.copy_from_slice(encrypted);
312
313                let len =
314                    match self
315                        .casep
316                        .sigma3_decrypt(self.crypto, fabric.ipk().op_key(), decrypted)
317                    {
318                        Ok(len) => len,
319                        Err(e) => {
320                            error!("Sigma3 AEAD decrypt failed: {}", e);
321                            return Ok(SCStatusCodes::InvalidParameter);
322                        }
323                    };
324                let decrypted = &decrypted[..len];
325                let decrypted_req: Sigma3Decrypt<'_> = match get_root_node_struct(decrypted)
326                    .and_then(|n| Sigma3Decrypt::from_tlv(&n))
327                {
328                    Ok(req) => req,
329                    Err(e) => {
330                        error!("Sigma3 decrypted TLV parse failed: {}", e);
331                        return Ok(SCStatusCodes::InvalidParameter);
332                    }
333                };
334
335                let initiator_noc = CertRef::new(TLVElement::new(decrypted_req.initiator_noc.0));
336                let initiator_icac = decrypted_req
337                    .initiator_icac
338                    .map(|icac| CertRef::new(TLVElement::new(icac.0)));
339
340                let mut buf = alloc!([0; CASE_LARGE_BUF_SIZE]); // TODO LARGE BUFFER
341                let buf = &mut buf[..];
342                if let Err(e) = self.casep.validate_certs(
343                    self.crypto,
344                    state.rtc.utc_time(),
345                    fabric,
346                    &initiator_noc,
347                    initiator_icac.as_ref(),
348                    buf,
349                ) {
350                    error!("Certificate Chain doesn't match: {}", e);
351                    Ok(SCStatusCodes::InvalidParameter)
352                } else if let Err(e) = self.casep.validate_peer_tbs_signature(
353                    self.crypto,
354                    decrypted_req.initiator_noc.0,
355                    decrypted_req.initiator_icac.map(|a| a.0),
356                    &initiator_noc,
357                    CanonPkcSignatureRef::try_new(decrypted_req.signature.0)?,
358                    buf,
359                ) {
360                    error!("Sigma3 Signature doesn't match: {}", e);
361                    Ok(SCStatusCodes::InvalidParameter)
362                } else {
363                    // Only now do we add this message to the TT Hash
364                    let mut peer_catids: NocCatIds = Default::default();
365                    initiator_noc.get_cat_ids(&mut peer_catids)?;
366                    self.casep.update_tt(exchange.rx()?.payload())?;
367
368                    let mut session_keys = MaybeUninit::<CaseSessionKeys>::uninit(); // TODO MEDIM BUFFER
369                    let session_keys = session_keys.init_with(CaseSessionKeys::init());
370                    self.casep.compute_session_keys(
371                        self.crypto,
372                        fabric.ipk().op_key(),
373                        session_keys,
374                    )?;
375
376                    let peer_addr = sess.get_peer_addr();
377
378                    let (dec_key, remaining) = session_keys
379                        .reference()
380                        .split::<AEAD_CANON_KEY_LEN, { AEAD_CANON_KEY_LEN * 2 }>();
381                    let (enc_key, att_challenge) =
382                        remaining.split::<AEAD_CANON_KEY_LEN, AEAD_CANON_KEY_LEN>();
383
384                    session.update_with_state(
385                        state,
386                        fabric.node_id(),
387                        initiator_noc.get_node_id()?,
388                        self.casep.peer_sessid(),
389                        self.casep.local_sessid(),
390                        peer_addr,
391                        SessionMode::Case {
392                            // Unwrapping is safe, because if the fabric index was 0, we would not be in here
393                            fab_idx: unwrap!(NonZeroU8::new(self.casep.local_fabric_idx())),
394                            cat_ids: peer_catids,
395                        },
396                        Some(dec_key),
397                        Some(enc_key),
398                        Some(att_challenge),
399                    )?;
400
401                    Ok(SCStatusCodes::SessionEstablishmentSuccess)
402                }
403            } else {
404                Ok(SCStatusCodes::NoSharedTrustRoots)
405            }
406        })?;
407
408        if matches!(status, SCStatusCodes::SessionEstablishmentSuccess) {
409            // Complete the reserved session and thus make the `Session` instance
410            // immediately available for use by the system.
411            //
412            // We need to do this _before_ we send the response to the peer, or else we risk missing
413            // (dropping) the first messages the peer would send us on the newly-established session,
414            // as it might start using it right after it receives the response, while it is still marked
415            // as reserved.
416            session.complete();
417        }
418
419        complete_with_status(exchange, status, &[]).await
420    }
421}