Skip to main content

rs_matter/
sc.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::borrow::Borrow;
19use core::future::Future;
20use core::mem::MaybeUninit;
21
22use num_derive::FromPrimitive;
23
24use crate::crypto::Crypto;
25use crate::dm::AttrChangeNotifier;
26use crate::error::{Error, ErrorCode};
27use crate::respond::ExchangeHandler;
28use crate::tlv::{FromTLV, ToTLV};
29use crate::transport::exchange::{Exchange, MessageMeta};
30use crate::utils::init::InitMaybeUninit;
31use crate::utils::storage::{ReadBuf, WriteBuf};
32
33use case::CaseResponder;
34use pase::PaseResponder;
35
36pub mod busy;
37pub mod case;
38pub mod checkin;
39#[cfg(feature = "groups")]
40pub mod mcsp;
41pub mod pase;
42
43/* Interaction Model ID as per the Matter Spec */
44pub const PROTO_ID_SECURE_CHANNEL: u16 = 0x00;
45
46#[derive(FromPrimitive, Debug, Copy, Clone, Eq, PartialEq)]
47#[cfg_attr(feature = "defmt", derive(defmt::Format))]
48pub enum OpCode {
49    MsgCounterSyncReq = 0x00,
50    MsgCounterSyncResp = 0x01,
51    MRPStandAloneAck = 0x10,
52    PBKDFParamRequest = 0x20,
53    PBKDFParamResponse = 0x21,
54    PASEPake1 = 0x22,
55    PASEPake2 = 0x23,
56    PASEPake3 = 0x24,
57    CASESigma1 = 0x30,
58    CASESigma2 = 0x31,
59    CASESigma3 = 0x32,
60    CASESigma2Resume = 0x33,
61    StatusReport = 0x40,
62    CheckIn = 0x50,
63}
64
65impl OpCode {
66    pub fn meta(&self) -> MessageMeta {
67        MessageMeta {
68            proto_id: PROTO_ID_SECURE_CHANNEL,
69            proto_opcode: *self as u8,
70            // Check-In is a fire-and-forget notification sent without MRP, like
71            // the standalone ack.
72            reliable: !matches!(self, Self::MRPStandAloneAck | Self::CheckIn),
73        }
74    }
75
76    pub fn is_tlv(&self) -> bool {
77        !matches!(
78            self,
79            Self::MRPStandAloneAck
80                | Self::StatusReport
81                | Self::MsgCounterSyncReq
82                | Self::MsgCounterSyncResp
83                | Self::CheckIn
84        )
85    }
86}
87
88impl From<OpCode> for MessageMeta {
89    fn from(op: OpCode) -> Self {
90        op.meta()
91    }
92}
93
94#[derive(Debug, Copy, Clone, PartialEq, Eq)]
95#[cfg_attr(feature = "defmt", derive(defmt::Format))]
96pub enum SCStatusCodes {
97    SessionEstablishmentSuccess = 0,
98    NoSharedTrustRoots = 1,
99    InvalidParameter = 2,
100    CloseSession = 3,
101    Busy = 4,
102    SessionNotFound = 5,
103}
104
105impl SCStatusCodes {
106    pub fn reliable(&self) -> bool {
107        // CloseSession, Busy and SessionNotFound are sent without the R flag raised
108        !matches!(
109            self,
110            SCStatusCodes::CloseSession | SCStatusCodes::Busy | SCStatusCodes::SessionNotFound
111        )
112    }
113
114    pub fn as_report<'a>(&self, payload: &'a [u8]) -> StatusReport<'a> {
115        let general_code = match self {
116            SCStatusCodes::SessionEstablishmentSuccess => GeneralCode::Success,
117            SCStatusCodes::CloseSession => GeneralCode::Success,
118            SCStatusCodes::Busy => GeneralCode::Busy,
119            SCStatusCodes::InvalidParameter
120            | SCStatusCodes::NoSharedTrustRoots
121            | SCStatusCodes::SessionNotFound => GeneralCode::Failure,
122        };
123
124        StatusReport {
125            general_code,
126            proto_id: PROTO_ID_SECURE_CHANNEL as u32,
127            proto_code: *self as u16,
128            proto_data: payload,
129        }
130    }
131}
132
133pub async fn complete_with_status(
134    exchange: &mut Exchange<'_>,
135    status_code: SCStatusCodes,
136    payload: &[u8],
137) -> Result<(), Error> {
138    exchange
139        .send_with(|_, wb| sc_write(wb, status_code, payload))
140        .await
141}
142
143pub fn sc_write(
144    wb: &mut WriteBuf,
145    status_code: SCStatusCodes,
146    payload: &[u8],
147) -> Result<Option<MessageMeta>, Error> {
148    status_code.as_report(payload).write(wb)?;
149
150    Ok(Some(
151        OpCode::StatusReport.meta().reliable(status_code.reliable()),
152    ))
153}
154
155#[allow(dead_code)]
156#[derive(FromPrimitive, PartialEq, Eq, Debug, Copy, Clone)]
157#[cfg_attr(feature = "defmt", derive(defmt::Format))]
158pub enum GeneralCode {
159    Success = 0,
160    Failure = 1,
161    BadPrecondition = 2,
162    OutOfRange = 3,
163    BadRequest = 4,
164    Unsupported = 5,
165    Unexpected = 6,
166    ResourceExhausted = 7,
167    Busy = 8,
168    Timeout = 9,
169    Continue = 10,
170    Aborted = 11,
171    InvalidArgument = 12,
172    NotFound = 13,
173    AlreadyExists = 14,
174    PermissionDenied = 15,
175    DataLoss = 16,
176}
177
178/// Represents the session parameters
179/// that might present in a "PBKDFParamRequest"/"PBKDFParamResponse" or "CASE-Sigma1"/"CASE-Sigma2" message
180#[derive(Default, Clone, FromTLV, ToTLV, Debug)]
181#[cfg_attr(feature = "defmt", derive(defmt::Format))]
182#[tlvargs(start = 1)]
183pub(crate) struct SessionParameters {
184    /// Session Idle Interval
185    pub(crate) sii: Option<u32>,
186    /// Session Active Interval
187    pub(crate) sai: Option<u32>,
188    /// Session Active Threshold
189    pub(crate) sat: Option<u16>,
190    /// Data Model Revision
191    pub(crate) dm_revision: Option<u16>,
192    /// Interaction Model Revision
193    pub(crate) im_revision: Option<u16>,
194    /// Specification Version
195    pub(crate) spec_version: Option<u32>,
196    /// Maximum number of paths per invoke
197    pub(crate) max_paths_per_invoke: Option<u16>,
198}
199
200/// Represents a Status Report message, as per "Appendix D: Status Report Messages" of the Matter Spec.
201#[derive(Debug, Clone)]
202#[cfg_attr(feature = "defmt", derive(defmt::Format))]
203pub struct StatusReport<'a> {
204    pub general_code: GeneralCode,
205    pub proto_id: u32,
206    pub proto_code: u16,
207    pub proto_data: &'a [u8],
208}
209
210impl<'a> StatusReport<'a> {
211    pub fn read<T>(pb: &'a mut ReadBuf<T>) -> Result<Self, Error>
212    where
213        T: Borrow<[u8]>,
214    {
215        Ok(Self {
216            general_code: num::FromPrimitive::from_u16(pb.le_u16()?)
217                .ok_or(ErrorCode::InvalidOpcode)?,
218            proto_id: pb.le_u32()?,
219            proto_code: pb.le_u16()?,
220            proto_data: pb.as_slice(),
221        })
222    }
223
224    pub fn write(&self, wb: &mut WriteBuf) -> Result<(), Error> {
225        wb.le_u16(self.general_code as u16)?;
226        wb.le_u32(self.proto_id)?;
227        wb.le_u16(self.proto_code)?;
228        wb.copy_from_slice(self.proto_data)?;
229
230        Ok(())
231    }
232}
233
234/// An extension point for the [`SecureChannel`] handler, letting a controller
235/// react to the Secure Channel messages that the accessory role only drops:
236/// incoming Check-In notifications and unsolicited Message Counter Sync
237/// responses.
238///
239/// Both methods default to dropping the message (matching the accessory role),
240/// so `()` is a valid no-op handler. A controller overrides the verb(s) it cares
241/// about; the handler receives the [`Exchange`] to read the message from.
242pub trait AsyncScHandler {
243    /// Handle an incoming Check-In message (Secure Channel opcode `CheckIn`).
244    async fn check_in(&self, _exchange: Exchange<'_>) -> Result<(), Error> {
245        warn!("Check-In: Unexpected Check-In message received; dropping");
246        Ok(())
247    }
248
249    /// Handle an unsolicited Message Counter Sync response
250    /// (opcode `MsgCounterSyncResp`).
251    async fn mcsp_resp(&self, _exchange: Exchange<'_>) -> Result<(), Error> {
252        warn!("MCSP: Unsolicited MsgCounterSyncResp received; dropping");
253        Ok(())
254    }
255}
256
257impl AsyncScHandler for () {}
258
259impl<T> AsyncScHandler for &T
260where
261    T: AsyncScHandler,
262{
263    async fn check_in(&self, exchange: Exchange<'_>) -> Result<(), Error> {
264        T::check_in(self, exchange).await
265    }
266
267    async fn mcsp_resp(&self, exchange: Exchange<'_>) -> Result<(), Error> {
268        T::mcsp_resp(self, exchange).await
269    }
270}
271
272/// Handle messages related to the Secure Channel
273pub struct SecureChannel<'a, C, H = ()> {
274    crypto: C,
275    notify: &'a dyn AttrChangeNotifier,
276    handler: H,
277}
278
279impl<'a, C: Crypto> SecureChannel<'a, C, ()> {
280    #[inline(always)]
281    pub const fn new(crypto: C, notify: &'a dyn AttrChangeNotifier) -> Self {
282        Self {
283            crypto,
284            notify,
285            handler: (),
286        }
287    }
288}
289
290impl<'a, C: Crypto, H: AsyncScHandler> SecureChannel<'a, C, H> {
291    /// Like [`new`](Self::new) but with a controller-side [`AsyncScHandler`] that
292    /// receives incoming Check-In / MCSP-response messages instead of dropping them.
293    #[inline(always)]
294    pub const fn new_with_handler(
295        crypto: C,
296        notify: &'a dyn AttrChangeNotifier,
297        handler: H,
298    ) -> Self {
299        Self {
300            crypto,
301            notify,
302            handler,
303        }
304    }
305
306    pub async fn handle(&self, mut exchange: Exchange<'_>) -> Result<(), Error> {
307        if exchange.rx().is_err() {
308            exchange.recv_fetch().await?;
309        }
310
311        let meta = exchange.rx()?.meta();
312        if meta.proto_id != PROTO_ID_SECURE_CHANNEL {
313            Err(ErrorCode::InvalidProto)?;
314        }
315
316        match meta.opcode()? {
317            OpCode::PBKDFParamRequest => {
318                let mut pase = MaybeUninit::uninit(); // TODO LARGE BUFFER
319                pase.init_with(PaseResponder::init(&self.crypto, self.notify))
320                    .handle(exchange)
321                    .await
322            }
323            OpCode::CASESigma1 => {
324                let mut case = MaybeUninit::uninit(); // TODO LARGE BUFFER
325                case.init_with(CaseResponder::init(&self.crypto))
326                    .handle(exchange)
327                    .await
328            }
329            #[cfg(feature = "groups")]
330            OpCode::MsgCounterSyncReq => {
331                // The receive path has already checked this landed on a
332                // group session with a destination matching one of our
333                // fabric node ids.
334                mcsp::respond(&self.crypto, exchange).await
335            }
336            OpCode::MsgCounterSyncResp => {
337                // Unsolicited in the accessory role; a controller may hook it.
338                self.handler.mcsp_resp(exchange).await
339            }
340            OpCode::CheckIn => {
341                // Unexpected in the accessory role (we are a Check-In *server*);
342                // a controller may hook it to receive Check-In notifications.
343                self.handler.check_in(exchange).await
344            }
345            opcode => {
346                error!("Invalid opcode: {:?}", opcode);
347                Err(ErrorCode::InvalidOpcode.into())
348            }
349        }
350    }
351}
352
353impl<C: Crypto, H: AsyncScHandler> ExchangeHandler for SecureChannel<'_, C, H> {
354    fn handle(&self, exchange: Exchange<'_>) -> impl Future<Output = Result<(), Error>> {
355        SecureChannel::handle(self, exchange)
356    }
357}
358
359/// Check the opcode of the received message like [`check_opcode`], additionally
360/// reporting a mismatch to the peer with a `StatusReport(FAILURE, INVALID_PARAMETER)`
361/// before bailing out with an error.
362async fn expect_opcode(exchange: &mut Exchange<'_>, opcode: OpCode) -> Result<(), Error> {
363    let result = check_opcode(exchange, opcode);
364
365    if let Err(err) = result {
366        if !exchange.rx()?.meta().is_sc_status() {
367            // Best-effort: the handshake has failed regardless of whether the
368            // report makes it to the peer, and the opcode mismatch is the more
369            // informative error to propagate.
370            let _ = complete_with_status(exchange, SCStatusCodes::InvalidParameter, &[]).await;
371        }
372
373        Err(err)
374    } else {
375        Ok(())
376    }
377}
378
379/// Check that the opcode of the received message matches the expected one.
380/// Logs an error if that's not the case, and if the opcode is `StatusReport`,
381/// it also logs the details of the status report.
382fn check_opcode(exchange: &Exchange<'_>, opcode: OpCode) -> Result<(), Error> {
383    let meta = exchange.rx()?.meta();
384    let their_opcode = meta.opcode::<OpCode>()?;
385
386    if their_opcode == opcode {
387        Ok(())
388    } else {
389        error!("Invalid opcode: {:?}, expected: {:?}", their_opcode, opcode);
390
391        if matches!(their_opcode, OpCode::StatusReport) {
392            let mut rb = ReadBuf::new(exchange.rx()?.payload());
393
394            // Show the status code details in the log
395            match StatusReport::read(&mut rb) {
396                Ok(status_report) => error!("Status Report: {:?}", status_report),
397                Err(e) => error!("Failed to parse Status Report: {:?}", e),
398            }
399        }
400
401        Err(ErrorCode::Invalid.into())
402    }
403}