Skip to main content

rs_matter/crypto/
canon.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//! Canonical representations of cryptographic material as per the Matter spec.
19
20use core::fmt::Debug;
21
22use crate::error::{Error, ErrorCode};
23use crate::tlv::{FromTLV, TLVElement, TLVTag, TLVWrite, ToTLV, TLV};
24use crate::utils::init::{init, zeroed, Init, IntoFallibleInit};
25
26/// Length of the hash returned by the hasher (`Crypto::hash`) in bytes.
27///
28/// As per the Matter spec, the hasher should be SHA-256.
29pub const HASH_LEN: usize = 32;
30
31/// Length of the SHA-1 hash returned by `Crypto::hash1` in bytes.
32pub const SHA1_HASH_LEN: usize = 20;
33
34/// Length of the HMAC hash returned by the HMAC hasher (`Crypto::hmac`) in bytes.
35///
36/// As per the Matter spec, the HMAC hasher should be HMAC-SHA-256.
37pub const HMAC_HASH_LEN: usize = HASH_LEN;
38
39/// Length of the canonical representation of an EC scalar in bytes.
40///
41/// As per the Matter spec, the curve used is secp256r1 (NIST P-256).
42pub const EC_CANON_SCALAR_LEN: usize = 32;
43
44/// Length of the canonical representation of an EC point in bytes.
45///
46/// As per the Matter spec, the curve used is secp256r1 (NIST P-256).
47pub const EC_CANON_POINT_LEN: usize = EC_CANON_SCALAR_LEN * 2 + 1;
48
49/// Length of the canonical representation of a public key in bytes.
50///
51/// As per the Matter spec, the used Public Key Cryptograqphy should be
52/// Elliptic-Curve based, and specifically secp256r1 (NIST P-256).
53///
54/// Note that this is the same as `EC_CANON_POINT_LEN`.
55pub const PKC_CANON_PUBLIC_KEY_LEN: usize = EC_CANON_POINT_LEN;
56
57/// Length of the canonical representation of a secret key in bytes.
58///
59/// As per the Matter spec, the used Public Key Cryptograqphy should be
60/// Elliptic-Curve based, and specifically secp256r1 (NIST P-256).
61///
62/// Note that this is the same as `EC_CANON_SCALAR_LEN`.
63pub const PKC_CANON_SECRET_KEY_LEN: usize = EC_CANON_SCALAR_LEN;
64
65/// Length of the canonical representation of a signature in bytes.
66///
67/// As per the Matter spec, the used Public Key Cryptograqphy should be
68/// Elliptic-Curve based, and specifically secp256r1 (NIST P-256).
69///
70/// Note that this is `2 * EC_CANON_SCALAR_LEN`, as the signature contains
71/// the (r, s) scalars computed using ECDSA.
72pub const PKC_SIGNATURE_LEN: usize = PKC_CANON_SECRET_KEY_LEN * 2;
73
74/// Length of the canonical representation of a shared secret in bytes.
75///
76/// As per the Matter spec, the used Public Key Cryptograqphy should be
77/// Elliptic-Curve based, and specifically secp256r1 (NIST P-256).
78///
79/// The shared secret is the ECDH computed value.
80pub const PKC_SHARED_SECRET_LEN: usize = 32;
81
82/// Length of the canonical representation of a 320-bit unsigned integer in bytes.
83///
84/// As per the Matter spec, this is used in the SPAKE2+ protocol.
85pub const UINT320_CANON_LEN: usize = 40;
86
87/// Length of the canonical representation of an AEAD key in bytes.
88///
89/// As per the Matter spec, the AEAD algorithm used is AES-CCM with 128-bit keys.
90pub const AEAD_CANON_KEY_LEN: usize = 16;
91
92/// Length of the nonce used in AEAD operations in bytes.
93///
94/// As per the Matter spec, the AEAD algorithm used is AES-CCM with a 13-byte nonce.
95pub const AEAD_NONCE_LEN: usize = 13;
96
97/// Length of the tag produced by AEAD operations in bytes.
98///
99/// As per the Matter spec, the AEAD algorithm used is AES-CCM with a 16-byte tag.
100pub const AEAD_TAG_LEN: usize = 16;
101
102macro_rules! canon {
103    ($len:expr, $zero: ident, $name:ident, $name_ref:ident) => {
104        /// Canonical representation of a $name.
105        #[allow(unused)]
106        pub type $name = $crate::crypto::CryptoSensitive<$len>;
107
108        /// Reference to a canonical $name.
109        #[allow(unused)]
110        pub type $name_ref<'a> = $crate::crypto::CryptoSensitiveRef<'a, $len>;
111
112        /// Zeroed $name.
113        #[allow(unused)]
114        pub const $zero: $name = $name::new();
115    };
116}
117
118pub(crate) use canon;
119
120canon!(HASH_LEN, HASH_ZEROED, Hash, HashRef);
121canon!(HMAC_HASH_LEN, HMAC_HASH_ZEROED, HmacHash, HmacHashRef);
122
123canon!(
124    UINT320_CANON_LEN,
125    UINT320_ZEROED,
126    CanonUint320,
127    CanonUint320Ref
128);
129
130canon!(
131    AEAD_CANON_KEY_LEN,
132    AEAD_KEY_ZEROED,
133    CanonAeadKey,
134    CanonAeadKeyRef
135);
136canon!(AEAD_NONCE_LEN, AEAD_NONCE_ZEROED, AeadNonce, AeadNonceRef);
137canon!(AEAD_TAG_LEN, AEAD_TAG_ZEROED, AeadTag, AeadTagRef);
138
139canon!(
140    PKC_CANON_PUBLIC_KEY_LEN,
141    PKC_PUBLIC_KEY_ZEROED,
142    CanonPkcPublicKey,
143    CanonPkcPublicKeyRef
144);
145canon!(
146    PKC_CANON_SECRET_KEY_LEN,
147    PKC_SECRET_KEY_ZEROED,
148    CanonPkcSecretKey,
149    CanonPkcSecretKeyRef
150);
151canon!(
152    PKC_SIGNATURE_LEN,
153    PKC_SIGNATURE_ZEROED,
154    CanonPkcSignature,
155    CanonPkcSignatureRef
156);
157canon!(
158    PKC_SHARED_SECRET_LEN,
159    PKC_SHARED_SECRET_ZEROED,
160    CanonPkcSharedSecret,
161    CanonPkcSharedSecretRef
162);
163
164canon!(
165    EC_CANON_SCALAR_LEN,
166    EC_SCALAR_ZEROED,
167    CanonEcScalar,
168    CanonEcScalarRef
169);
170canon!(
171    EC_CANON_POINT_LEN,
172    EC_POINT_ZEROED,
173    CanonEcPoint,
174    CanonEcPointRef
175);
176
177/// A cryptographic material represented in a cross-platform way,
178/// as a fixed-length array in a well-defined format.
179///
180/// Thus, it can be imported into any `Crypto` provider and exported from it without
181/// worrying about endianness or other platform-specific representation issues.
182///
183/// The reason for wrapping the array with a newtype is so that the following
184/// protection measures are taken:
185/// - The material is not accidentally printed in logs or debug output.
186/// - The material has a single `access` / `access_mut` method and deliberately
187///   does not implement `Deref` or `AsRef` traits to avoid accidental leakage.
188/// - The material is zeroed out when dropped; note however that this has a limited use case,
189///   in Rust, because of the Rust move semantics.
190///
191/// Regarding sensitivity, `rs-matter` takes a radical approach and assumes that
192/// *all* cryptographic material is sensitive. Including, but not limited to:
193/// - Secret keys (obviously)
194/// - Signatures (because they can leak information about the secret key)
195/// - Hashes (because they can be pre-images of sensitive data)
196/// - Public keys (just in case)
197#[derive(Clone)]
198pub struct CryptoSensitive<const N: usize> {
199    /// The underlying data array that needs to be protected.
200    data: [u8; N],
201}
202
203impl<const N: usize> CryptoSensitive<N> {
204    /// Create a new zeroed `CryptoSensitive` instance.
205    #[inline(always)]
206    pub const fn new() -> Self {
207        Self { data: [0u8; N] }
208    }
209
210    /// Create a new `CryptoSensitive` instance by loading data from the provided reference.
211    pub const fn new_from_ref(other: CryptoSensitiveRef<'_, N>) -> Self {
212        let mut this = Self::new();
213
214        this.load(other);
215
216        this
217    }
218
219    /// Return an in-place initializer for a zeroed `CryptoSensitive` instance.
220    #[inline(always)]
221    pub fn init() -> impl Init<Self> {
222        init!(Self {
223            data <- zeroed(),
224        })
225    }
226
227    /// Zeroizes the cryptographic material held by this instance.
228    pub fn zeroize(&mut self) {
229        // TODO: Depend on the `zeroize` crate to ensure that the compiler does not optimize this out.
230        // TODO: Implement some sort of pinning to ensure that the compiler does move this type when on-stack.
231        self.data.fill(0);
232    }
233
234    /// Get a reference to this cryptographic material.
235    pub const fn reference(&self) -> CryptoSensitiveRef<'_, N> {
236        CryptoSensitiveRef::new(&self.data)
237    }
238
239    /// Load data from another cryptographic material reference.
240    pub const fn load(&mut self, other: CryptoSensitiveRef<'_, N>) {
241        self.load_from_array(other.access());
242    }
243
244    /// Load data from a byte array.
245    pub const fn load_from_array(&mut self, data: &[u8; N]) {
246        self.data.copy_from_slice(data);
247    }
248
249    /// Try to load data from a byte slice.
250    ///
251    /// Returns an error if the slice length does not match the expected length.
252    pub fn try_load_from_slice(&mut self, data: &[u8]) -> Result<(), Error> {
253        if data.len() != N {
254            return Err(ErrorCode::InvalidData.into());
255        }
256
257        self.data.copy_from_slice(data);
258
259        Ok(())
260    }
261
262    /// Access the underlying data as a byte array reference.
263    ///
264    /// NOTE: care should be taken when using this method, as it exposes the sensitive data.
265    pub const fn access(&self) -> &[u8; N] {
266        &self.data
267    }
268
269    /// Access the underlying data as a mutable byte array reference.
270    ///
271    /// NOTE: care should be taken when using this method, as it exposes the sensitive data.
272    pub const fn access_mut(&mut self) -> &mut [u8; N] {
273        &mut self.data
274    }
275}
276
277impl<const N: usize> Drop for CryptoSensitive<N> {
278    fn drop(&mut self) {
279        self.zeroize();
280    }
281}
282
283impl<const N: usize> Default for CryptoSensitive<N> {
284    fn default() -> Self {
285        Self::new()
286    }
287}
288
289impl<const N: usize> Debug for CryptoSensitive<N> {
290    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
291        write!(f, "CryptoSensitive<{}>(**hidden**)", N)
292    }
293}
294
295#[cfg(feature = "defmt")]
296impl<const N: usize> defmt::Format for CryptoSensitive<N> {
297    fn format(&self, f: defmt::Formatter) {
298        defmt::write!(f, "CryptoSensitive<{}>(**hidden**)", N);
299    }
300}
301
302impl<const N: usize> From<CryptoSensitiveRef<'_, N>> for CryptoSensitive<N> {
303    fn from(other: CryptoSensitiveRef<'_, N>) -> Self {
304        let mut material = CryptoSensitive::new();
305
306        material.load(other);
307
308        material
309    }
310}
311
312impl<const N: usize> From<&[u8; N]> for CryptoSensitive<N> {
313    fn from(data: &[u8; N]) -> Self {
314        let mut material = CryptoSensitive::new();
315
316        material.load_from_array(data);
317
318        material
319    }
320}
321
322impl<const N: usize> From<[u8; N]> for CryptoSensitive<N> {
323    fn from(data: [u8; N]) -> Self {
324        let mut material = CryptoSensitive::new();
325
326        material.load_from_array(&data);
327
328        material
329    }
330}
331
332impl<const N: usize> TryFrom<&[u8]> for CryptoSensitive<N> {
333    type Error = Error;
334
335    fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
336        let mut material = CryptoSensitive::new();
337
338        material.try_load_from_slice(data)?;
339
340        Ok(material)
341    }
342}
343
344impl<'a, const N: usize> FromTLV<'a> for CryptoSensitive<N> {
345    fn from_tlv(element: &TLVElement<'a>) -> Result<Self, crate::error::Error> {
346        Ok(Self {
347            data: element
348                .str()?
349                .try_into()
350                .map_err(|_| ErrorCode::ConstraintError)?,
351        })
352    }
353
354    fn init_from_tlv(element: TLVElement<'a>) -> impl Init<Self, Error> {
355        Init::chain(Self::init().into_fallible(), move |this| {
356            let data = element.str()?;
357            if data.len() != N {
358                Err(ErrorCode::ConstraintError)?;
359            }
360
361            this.access_mut().copy_from_slice(data);
362
363            Ok(())
364        })
365    }
366}
367
368impl<const N: usize> ToTLV for CryptoSensitive<N> {
369    fn to_tlv<W: TLVWrite>(&self, tag: &TLVTag, mut tw: W) -> Result<(), Error> {
370        tw.str(tag, &self.data)
371    }
372
373    fn tlv_iter(&self, tag: TLVTag) -> impl Iterator<Item = Result<TLV<'_>, Error>> {
374        TLV::str(tag, self.data.as_slice()).into_tlv_iter()
375    }
376}
377
378/// A reference to cryptographic material.
379/// The non-owned equivalent of `CryptoSensitive<N>`.
380#[derive(Copy, Clone)]
381pub struct CryptoSensitiveRef<'a, const N: usize> {
382    data: &'a [u8; N],
383}
384
385impl<'a, const N: usize> CryptoSensitiveRef<'a, N> {
386    /// Create a new `CryptoSensitiveRef` instance from a byte array reference.
387    #[inline(always)]
388    pub const fn new(material: &'a [u8; N]) -> Self {
389        Self { data: material }
390    }
391
392    /// Create a new `CryptoSensitiveRef` instance from a byte slice.
393    ///
394    /// Panics if the slice length does not match the expected length.
395    #[inline(always)]
396    pub fn new_from_slice(material: &'a [u8]) -> Self {
397        assert_eq!(material.len(), N);
398
399        Self::new(Self::as_array(material).unwrap()) // TODO
400    }
401
402    /// Try to create a new `CryptoSensitiveRef` instance from a byte slice.
403    ///
404    /// Returns an error if the slice length does not match the expected length.
405    #[inline(always)]
406    pub fn try_new(material: &'a [u8]) -> Result<Self, Error> {
407        if material.len() != N {
408            Err(ErrorCode::InvalidData)?;
409        }
410
411        Ok(Self::new(Self::as_array(material).unwrap())) // TODO
412    }
413
414    /// Split this reference into two references of the specified lengths.
415    ///
416    /// Panics if the sum of the specified lengths does not match the length of this reference.
417    pub fn split<const M1: usize, const M2: usize>(
418        &self,
419    ) -> (CryptoSensitiveRef<'a, M1>, CryptoSensitiveRef<'a, M2>) {
420        let (left, right) = self.data.split_at(M1);
421
422        (
423            CryptoSensitiveRef::new_from_slice(left),
424            CryptoSensitiveRef::new_from_slice(right),
425        )
426    }
427
428    /// Access the underlying data as a byte array reference.
429    ///
430    /// NOTE: care should be taken when using this method, as it exposes the sensitive data.
431    pub const fn access(&self) -> &'a [u8; N] {
432        self.data
433    }
434
435    // TODO: `as_array` is not yet const fn in Rust core
436    const fn as_array<const L: usize>(slice: &'a [u8]) -> Option<&'a [u8; L]> {
437        if slice.len() == L {
438            let ptr = slice.as_ptr() as *const [u8; L];
439
440            // SAFETY: The underlying array of a slice can be reinterpreted as an actual array `[T; N]` if `N` is not greater than the slice's length.
441            let me = unsafe { &*ptr };
442            Some(me)
443        } else {
444            None
445        }
446    }
447}
448
449impl<const N: usize> Debug for CryptoSensitiveRef<'_, N> {
450    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
451        write!(f, "CryptoSensitiveRef<{}>(**hidden**)", N)
452    }
453}
454
455#[cfg(feature = "defmt")]
456impl<const N: usize> defmt::Format for CryptoSensitiveRef<'_, N> {
457    fn format(&self, f: defmt::Formatter) {
458        defmt::write!(f, "CryptoSensitiveRef<{}>(**hidden**)", N);
459    }
460}
461
462impl<'a, const N: usize> From<&'a CryptoSensitive<N>> for CryptoSensitiveRef<'a, N> {
463    fn from(cs: &'a CryptoSensitive<N>) -> Self {
464        cs.reference()
465    }
466}
467
468impl<'a, const N: usize> From<&'a [u8; N]> for CryptoSensitiveRef<'a, N> {
469    fn from(data: &'a [u8; N]) -> Self {
470        Self::new(data)
471    }
472}
473
474impl<'a, const N: usize> TryFrom<&'a [u8]> for CryptoSensitiveRef<'a, N> {
475    type Error = Error;
476
477    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
478        Self::try_new(data)
479    }
480}