Skip to main content

matter_commissioning/noc/
error.rs

1//! Error type, RNG abstraction, and `ring`-backed default RNG for the
2//! `noc` module.
3
4#![forbid(unsafe_code)]
5
6use thiserror::Error;
7
8/// Errors produced by `matter-commissioning::noc`.
9///
10/// Variants are coarse-grained — each maps to a distinct caller-side
11/// remediation path. See the M6.3 design doc's "Information leakage in
12/// error variants" table for the audit reasoning.
13#[derive(Debug, Error)]
14#[non_exhaustive]
15pub enum NocError {
16    /// The NOCSR TLV outer envelope was malformed.
17    #[error("NOCSR TLV parse failure")]
18    NocsrParse(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
19
20    /// The embedded PKCS#10 CSR could not be parsed.
21    #[error("embedded PKCS#10 CSR parse failure")]
22    CsrParse(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
23
24    /// The PKCS#10 CSR's self-signature did not verify.
25    #[error("CSR self-signature verification failed")]
26    BadCsrSelfSignature,
27
28    /// The `CSRNonce` echoed by the device did not match the value the
29    /// commissioner sent in `CSRRequest`.
30    #[error("CSRNonce did not match commissioner-issued value")]
31    NonceMismatch,
32
33    /// The DAC's signature over `NOCSR_elements || attestation_challenge`
34    /// did not verify. Coarse by design (see design doc).
35    #[error("DAC attestation signature over NOCSR failed")]
36    BadCsrAttestationSignature,
37
38    /// The CSR's public key was not a valid P-256 SEC1 uncompressed point.
39    #[error("CSR public key is not a valid P-256 point")]
40    InvalidCsrPublicKey,
41
42    /// A Matter DN attribute (e.g., a `CaseAuthenticatedTag`) could not be
43    /// constructed from the caller-supplied values.
44    #[error("Matter DN attribute construction failed")]
45    DnAttributeOverflow,
46
47    /// NOC certificate construction via the matter-cert builder failed.
48    #[error("NOC certificate construction failed")]
49    CertBuild(#[source] matter_cert::Error),
50
51    /// The fabric's root signer rejected the signing operation.
52    #[error("NOC signing failed")]
53    SigningFailed(#[source] matter_crypto::SignerError),
54
55    /// The system RNG (or a caller-supplied stub) failed.
56    #[error("RNG failure")]
57    Rng,
58
59    /// An `OpCreds` cluster command codec failed.
60    #[error("OpCreds cluster payload codec error")]
61    ClusterCodec(#[source] matter_codec::Error),
62
63    /// An `OpCreds` cluster response was *structurally* malformed — the TLV
64    /// itself decoded, but its shape violated the expected schema (wrong
65    /// container kind / tag, duplicate field, a fixed-width field with the
66    /// wrong length, or a required field absent).
67    ///
68    /// Distinct from [`NocError::ClusterCodec`], which wraps a low-level codec
69    /// failure (e.g. a truncated buffer). A structural mismatch must not be
70    /// reported as a codec EOF — the carried `&'static str` names what was
71    /// expected so callers and logs get an accurate label.
72    #[error("OpCreds cluster response is structurally malformed: {0}")]
73    MalformedResponse(&'static str),
74}
75
76impl From<matter_codec::Error> for NocError {
77    fn from(e: matter_codec::Error) -> Self {
78        Self::ClusterCodec(e)
79    }
80}
81
82/// Pluggable random-byte source used by `noc` for `CSRNonce`, NOC serial,
83/// IPK, and any future secret-material draw. Caller-supplied so tests
84/// can pass deterministic stubs.
85pub trait NocRng: Send + Sync + std::fmt::Debug {
86    /// Fill `dest` with cryptographically secure random bytes.
87    ///
88    /// # Errors
89    ///
90    /// Returns [`NocError::Rng`] if the underlying RNG fails. For
91    /// `SystemNocRng` this is effectively never; for caller-supplied
92    /// implementations it may surface IO or device errors.
93    fn fill(&self, dest: &mut [u8]) -> Result<(), NocError>;
94}
95
96/// Production default: wraps [`ring::rand::SystemRandom`].
97#[derive(Debug, Default)]
98pub struct SystemNocRng;
99
100impl NocRng for SystemNocRng {
101    fn fill(&self, dest: &mut [u8]) -> Result<(), NocError> {
102        use ring::rand::SecureRandom;
103        ring::rand::SystemRandom::new()
104            .fill(dest)
105            .map_err(|_| NocError::Rng)
106    }
107}
108
109#[cfg(test)]
110#[allow(clippy::unwrap_used)] // Test-code carve-out: see CLAUDE.md.
111mod tests {
112    use super::*;
113
114    #[test]
115    fn system_rng_fills_distinct_bytes() {
116        // Two draws of 32 bytes each must (with overwhelming probability)
117        // differ — not a strict guarantee but a useful smoke test that
118        // SystemNocRng is wired to a real entropy source.
119        let mut a = [0u8; 32];
120        let mut b = [0u8; 32];
121        SystemNocRng.fill(&mut a).unwrap();
122        SystemNocRng.fill(&mut b).unwrap();
123        assert_ne!(a, b, "two random draws collided — RNG is not wired");
124        assert_ne!(a, [0u8; 32], "RNG returned all zeros");
125    }
126}