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;
28use crate::crypto::{
29    CanonPkcPublicKeyRef, CanonPkcSignature, CanonPkcSignatureRef, Crypto, Hash, AEAD_CANON_KEY_LEN,
30};
31use crate::error::{Error, ErrorCode};
32use crate::sc::{complete_with_status, GeneralCode, OpCode, SCStatusCodes, StatusReport};
33use crate::tlv::{get_root_node_struct, FromTLV, OctetStr, TLVElement, TLVTag, TLVWrite};
34use crate::transport::exchange::Exchange;
35use crate::transport::session::{NocCatIds, ReservedSession, SessionMode};
36use crate::utils::init::InitMaybeUninit;
37use crate::utils::storage::ReadBuf;
38
39use super::casep::{CaseP, CaseRandom, CaseRandomRef, CaseSessionKeys, CASE_RESUMPTION_ID_ZEROED};
40use super::CASE_LARGE_BUF_SIZE;
41
42/// Sigma2 Response structure, parsed from the responder's Sigma2 message.
43#[derive(FromTLV, Debug)]
44#[cfg_attr(feature = "defmt", derive(defmt::Format))]
45#[tlvargs(start = 1, lifetime = "'a")]
46struct Sigma2Resp<'a> {
47    /// The responder's random value
48    responder_random: OctetStr<'a>,
49    /// The responder's session ID
50    responder_sessid: u16,
51    /// The responder's ephemeral public key
52    responder_eph_pub_key: OctetStr<'a>,
53    /// The encrypted TBE2 payload
54    encrypted2: OctetStr<'a>,
55}
56
57/// Decrypted TBE data from Sigma2
58#[derive(FromTLV)]
59#[tlvargs(start = 1, lifetime = "'a")]
60struct TBEData2Decrypt<'a> {
61    responder_noc: OctetStr<'a>,
62    responder_icac: Option<OctetStr<'a>>,
63    signature: OctetStr<'a>,
64    resumption_id: OctetStr<'a>,
65}
66
67/// CASE Initiator for establishing secure sessions with Matter devices using operational
68/// certificates.
69///
70/// This implements the controller side of the CASE protocol.
71/// The typical flow is:
72///
73/// 1. Create an exchange to the target device
74/// 2. Call `CaseInitiator::initiate()` with the fabric index and peer node ID
75/// 3. On success, the exchange's session is upgraded to a secure CASE session
76pub struct CaseInitiator<'a, C: Crypto + 'a> {
77    casep: CaseP<'a, C>,
78    /// The peer's node ID (used to verify responder NOC in process_sigma2)
79    peer_node_id: u64,
80    /// Our ephemeral secret key (retained from start_initiator for ECDH in process_sigma2)
81    secret_key: Option<C::SecretKey<'a>>,
82}
83
84impl<'a, C: Crypto + 'a> CaseInitiator<'a, C> {
85    /// Create a new CASE initiator
86    const fn new(peer_node_id: u64) -> Self {
87        Self {
88            casep: CaseP::new(),
89            peer_node_id,
90            secret_key: None,
91        }
92    }
93
94    /// Initiate a CASE handshake with a Matter device.
95    ///
96    /// This performs the complete CASE handshake:
97    /// 1. Send Sigma1 (initiator_random, session_id, destination_id, eph_pub_key)
98    /// 2. Receive Sigma2 (responder_random, session_id, eph_pub_key, encrypted TBE2)
99    /// 3. Send Sigma3 (encrypted TBE3)
100    /// 4. Receive StatusReport
101    ///
102    /// On success, the session is upgraded to a secure CASE session.
103    ///
104    /// # Arguments
105    /// - `exchange` - An exchange to the target device
106    /// - `crypto` - The crypto implementation
107    /// - `fab_idx` - The fabric index to use for the handshake
108    /// - `peer_node_id` - The node ID of the target device
109    pub async fn initiate(
110        exchange: &mut Exchange<'_>,
111        crypto: &'a C,
112        fab_idx: NonZeroU8,
113        peer_node_id: u64,
114    ) -> Result<(), Error> {
115        // Step 1: Reserve a session slot
116        let mut session = ReservedSession::reserve(exchange.matter(), crypto).await?;
117
118        let mut initiator = Self::new(peer_node_id);
119
120        let mut random = MaybeUninit::<CaseRandom>::uninit();
121        let random = random.init_with(CaseRandom::init());
122
123        let mut dest_id = MaybeUninit::<Hash>::uninit();
124        let dest_id = dest_id.init_with(Hash::init());
125
126        // Step 2: Prepare Sigma1 parameters
127        let local_sessid = exchange.with_state(|state| {
128            let local_sessid = state.sessions.get_next_sess_id();
129
130            let fabric = state.fabrics.fabric(fab_idx)?;
131
132            let secret_key = initiator.casep.start_initiator(
133                crypto,
134                fabric,
135                peer_node_id,
136                local_sessid,
137                random,
138                dest_id,
139            )?;
140
141            initiator.secret_key = Some(secret_key);
142
143            Ok(local_sessid)
144        })?;
145
146        // Step 3: Build and send Sigma1
147        let mut tt_updated = false;
148        exchange
149            .send_with(|_, tw| {
150                tw.start_struct(&TLVTag::Anonymous)?;
151                tw.str(&TLVTag::Context(1), random.access())?;
152                tw.u16(&TLVTag::Context(2), local_sessid)?;
153                tw.str(&TLVTag::Context(3), dest_id.access())?;
154                tw.str(&TLVTag::Context(4), initiator.casep.our_pub_key().access())?;
155                tw.end_container()?;
156
157                if !tt_updated {
158                    initiator.casep.update_tt(tw.as_slice())?;
159                    tt_updated = true;
160                }
161
162                Ok(Some(OpCode::CASESigma1.into()))
163            })
164            .await?;
165
166        // Step 4: Receive Sigma2
167        exchange.recv_fetch().await?;
168
169        {
170            let rx = exchange.rx()?;
171            let meta = rx.meta();
172
173            // Check for StatusReport error first
174            if meta.proto_opcode == OpCode::StatusReport as u8 {
175                let mut rb = ReadBuf::new(rx.payload());
176                let status = StatusReport::read(&mut rb)?;
177                error!(
178                    "CASE Sigma1 failed: general={:?}, proto_code={}",
179                    status.general_code, status.proto_code
180                );
181                return Err(ErrorCode::Invalid.into());
182            }
183
184            // Verify opcode is CASESigma2
185            if meta.proto_opcode != OpCode::CASESigma2 as u8 {
186                error!(
187                    "Unexpected opcode: expected CASESigma2, got {}",
188                    meta.proto_opcode
189                );
190                return Err(ErrorCode::InvalidOpcode.into());
191            }
192        }
193
194        // Step 5: Decrypt Sigma2 TBE and validate
195        let (peer_catids, _resumption_id) = {
196            let rx = exchange.rx()?;
197            let raw_sigma2_payload = rx.payload();
198
199            let sigma2 = Sigma2Resp::from_tlv(&get_root_node_struct(raw_sigma2_payload)?)?;
200
201            let result = exchange.with_state(|state| {
202                // Copy encrypted2 to a mutable stack buffer for in-place decryption
203                let mut encrypted2_buf = alloc!([0u8; CASE_LARGE_BUF_SIZE]); // TODO LARGE BUFFER
204
205                if sigma2.encrypted2.0.len() > encrypted2_buf.len() {
206                    error!("Sigma2 encrypted data too large");
207                    return Err(ErrorCode::BufferTooSmall.into());
208                }
209
210                let encrypted2 = &mut encrypted2_buf[..sigma2.encrypted2.0.len()];
211                encrypted2.copy_from_slice(sigma2.encrypted2.0);
212
213                let peer_random = CaseRandomRef::try_new(sigma2.responder_random.0)?;
214                let peer_sessid = sigma2.responder_sessid;
215                let peer_eph_pub_key =
216                    CanonPkcPublicKeyRef::try_new(sigma2.responder_eph_pub_key.0)?;
217
218                let fabric = state.fabrics.fabric(fab_idx)?;
219
220                let secret_key = initiator
221                    .secret_key
222                    .as_ref()
223                    .ok_or(ErrorCode::InvalidState)?;
224
225                // Decrypt TBE2 (symmetric with sigma3_decrypt on the responder side)
226                let len = initiator
227                    .casep
228                    .sigma2_decrypt(
229                        crypto,
230                        fabric,
231                        secret_key,
232                        raw_sigma2_payload,
233                        peer_random,
234                        peer_sessid,
235                        peer_eph_pub_key,
236                        encrypted2,
237                    )
238                    .inspect_err(|e| {
239                        error!("Failed to decrypt Sigma2 TBE: {}", e);
240                    })?;
241
242                // Clear the secret key after ECDH
243                initiator.secret_key = None;
244
245                let decrypted = &encrypted2[..len];
246                let decrypted_data = TBEData2Decrypt::from_tlv(&get_root_node_struct(decrypted)?)?;
247
248                // Validate certificate chain
249                let responder_noc = CertRef::new(TLVElement::new(decrypted_data.responder_noc.0));
250                let icac_cert = decrypted_data
251                    .responder_icac
252                    .as_ref()
253                    .map(|icac| CertRef::new(TLVElement::new(icac.0)));
254
255                let mut tmp_buf = alloc!([0u8; CASE_LARGE_BUF_SIZE]); // TODO LARGE BUFFER
256                initiator
257                    .casep
258                    .validate_certs(
259                        crypto,
260                        state.rtc.utc_time(),
261                        fabric,
262                        &responder_noc,
263                        icac_cert.as_ref(),
264                        &mut tmp_buf[..],
265                    )
266                    .inspect_err(|e| {
267                        error!("Certificate chain doesn't match: {}", e);
268                    })?;
269
270                // Verify the responder's node ID matches the expected peer
271                if responder_noc.get_node_id()? != initiator.peer_node_id {
272                    error!(
273                        "Responder node ID doesn't match expected peer: expected {}, got {}",
274                        initiator.peer_node_id,
275                        responder_noc.get_node_id()?
276                    );
277
278                    Err(ErrorCode::Invalid)?;
279                }
280
281                // Verify signature
282                initiator
283                    .casep
284                    .validate_peer_tbs_signature(
285                        crypto,
286                        decrypted_data.responder_noc.0,
287                        decrypted_data.responder_icac.map(|a| a.0),
288                        &responder_noc,
289                        CanonPkcSignatureRef::try_new(decrypted_data.signature.0)?,
290                        &mut tmp_buf[..],
291                    )
292                    .inspect_err(|e| {
293                        error!("Sigma2 signature doesn't match: {}", e);
294                    })?;
295
296                // Extract CAT IDs
297                let mut peer_catids: NocCatIds = Default::default();
298                responder_noc.get_cat_ids(&mut peer_catids)?;
299
300                // Capture resumption ID
301                let mut resumption_id = CASE_RESUMPTION_ID_ZEROED;
302                resumption_id
303                    .access_mut()
304                    .copy_from_slice(decrypted_data.resumption_id.0);
305
306                Ok((peer_catids, resumption_id))
307            });
308
309            if result.is_err() {
310                complete_with_status(exchange, SCStatusCodes::InvalidParameter, &[]).await?;
311            }
312
313            result
314        }?;
315
316        // Step 6: Compute Sigma3 signature (needs fabric borrow, must drop before await)
317        let mut signature = MaybeUninit::<CanonPkcSignature>::uninit();
318        let signature = signature.init_with(CanonPkcSignature::init());
319
320        exchange.with_state(|state| {
321            let fabric = state.fabrics.fabric(fab_idx)?;
322
323            // Use a temporary buffer for the TBS data
324            let mut tmp_buf = alloc!([0u8; CASE_LARGE_BUF_SIZE]);
325            initiator
326                .casep
327                .compute_sigma3_signature(crypto, fabric, &mut tmp_buf[..], signature)
328        })?;
329
330        // Step 7: Build and send Sigma3
331        let mut tt_updated = false;
332        exchange
333            .send_with(|exchange_ref, tw| {
334                exchange_ref.with_state(|state| {
335                    let fabric = state.fabrics.fabric(fab_idx)?;
336
337                    tw.start_struct(&TLVTag::Anonymous)?;
338                    tw.str_cb(&TLVTag::Context(1), |buf| {
339                        initiator
340                            .casep
341                            .sigma3_encrypt(crypto, fabric, signature.reference(), buf)
342                    })?;
343                    tw.end_container()?;
344
345                    if !tt_updated {
346                        initiator.casep.update_tt(tw.as_slice())?;
347                        tt_updated = true;
348                    }
349
350                    Ok(Some(OpCode::CASESigma3.into()))
351                })
352            })
353            .await?;
354
355        // Step 8: Receive StatusReport
356        exchange.recv_fetch().await?;
357
358        {
359            let rx = exchange.rx()?;
360            let meta = rx.meta();
361
362            if meta.proto_opcode != OpCode::StatusReport as u8 {
363                error!(
364                    "Unexpected opcode: expected StatusReport, got {}",
365                    meta.proto_opcode
366                );
367                return Err(ErrorCode::InvalidOpcode.into());
368            }
369
370            let mut rb = ReadBuf::new(rx.payload());
371            let status = StatusReport::read(&mut rb)?;
372
373            if status.general_code != GeneralCode::Success
374                || status.proto_code != SCStatusCodes::SessionEstablishmentSuccess as u16
375            {
376                error!(
377                    "CASE failed: general={:?}, proto_code={}",
378                    status.general_code, status.proto_code
379                );
380                return Err(ErrorCode::Invalid.into());
381            }
382        }
383
384        // Step 9: Derive session keys and complete the session
385        {
386            let mut session_keys = MaybeUninit::<CaseSessionKeys>::uninit();
387            let session_keys = session_keys.init_with(CaseSessionKeys::init());
388
389            let (peer_addr, local_node_id) = exchange.with_state(|state| {
390                let sess = exchange.id().session(&mut state.sessions);
391
392                let fabric = state.fabrics.fabric(fab_idx)?;
393
394                initiator.casep.compute_session_keys(
395                    crypto,
396                    fabric.ipk().op_key(),
397                    session_keys,
398                )?;
399
400                Ok((sess.get_peer_addr(), fabric.node_id()))
401            })?;
402
403            // For initiator: first key = I2R (enc_key), second = R2I (dec_key)
404            let (enc_key, remaining) = session_keys
405                .reference()
406                .split::<AEAD_CANON_KEY_LEN, { AEAD_CANON_KEY_LEN * 2 }>();
407            let (dec_key, att_challenge) =
408                remaining.split::<AEAD_CANON_KEY_LEN, AEAD_CANON_KEY_LEN>();
409
410            session.update(
411                local_node_id,
412                peer_node_id,
413                initiator.casep.peer_sessid(),
414                initiator.casep.local_sessid(),
415                peer_addr,
416                SessionMode::Case {
417                    fab_idx,
418                    cat_ids: peer_catids,
419                },
420                Some(dec_key),
421                Some(enc_key),
422                Some(att_challenge),
423            )?;
424        }
425
426        session.complete();
427
428        exchange.acknowledge().await?;
429
430        info!(
431            "CASE session established: local_sessid={}, peer_sessid={}",
432            initiator.casep.local_sessid(),
433            initiator.casep.peer_sessid()
434        );
435
436        Ok(())
437    }
438}