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}