matter_commissioning/attestation/error.rs
1//! Error type for the attestation module.
2//!
3//! M6.2.1 shipped only the [`AttestationError::Parse`] variant.
4//! M6.2.2 added six chain-validation outcomes. M6.2.3 adds the single
5//! signature-verification outcome
6//! ([`AttestationError::BadResponseSignature`]).
7
8use thiserror::Error;
9
10use crate::attestation::extensions::VendorId;
11
12/// Errors produced by device attestation verification.
13///
14/// `#[non_exhaustive]` so future phases can add variants without a
15/// breaking change.
16#[derive(Debug, Error)]
17#[non_exhaustive]
18pub enum AttestationError {
19 /// The DER bytes passed to one of [`crate::attestation::x509::Dac`],
20 /// [`crate::attestation::x509::Pai`], or
21 /// [`crate::attestation::x509::Paa`]'s `from_der` constructor failed
22 /// to parse, or failed a Matter-specific subject-DN structural check
23 /// (missing required VID/PID attribute, or — for
24 /// [`crate::attestation::x509::Paa`] — a forbidden PID attribute).
25 #[error("X.509 parse failure")]
26 Parse(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
27
28 /// Path validation rejected the chain for a reason not captured by
29 /// a more specific variant. Sources a boxed `webpki::Error`
30 /// (downcastable via `Error::downcast_ref` on the trait object
31 /// returned by `source()`) so callers who care about the
32 /// underlying webpki kind can still inspect it without our
33 /// public API mentioning webpki by type.
34 #[error("certificate chain validation failed")]
35 InvalidChain(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
36
37 /// One of the certs in the chain was outside its validity window
38 /// at the supplied [`matter_cert::time::MatterTime`].
39 #[error("certificate expired or not yet valid")]
40 TimeBoundsViolation,
41
42 /// A non-CA cert was marked `BasicConstraints.cA = true`, or the
43 /// path-length-constraint was violated.
44 #[error("BasicConstraints violation")]
45 BasicConstraintsViolation,
46
47 /// No PAA in the supplied [`crate::attestation::PaaTrustStore`]
48 /// matched the PAI's issuer.
49 #[error("PAA not in trust store")]
50 UntrustedRoot,
51
52 /// DAC subject [`VendorId`] did not equal PAI subject [`VendorId`]
53 /// (Matter §6.2.3 requires equality).
54 #[error("VID mismatch: DAC={dac:?} PAI={pai:?}")]
55 VidMismatch {
56 /// [`VendorId`] observed on the DAC subject.
57 dac: VendorId,
58 /// [`VendorId`] observed on the PAI subject.
59 pai: VendorId,
60 },
61
62 /// PAI is product-scoped (`subject_pid` is `Some`) and its
63 /// [`crate::attestation::ProductId`] differs from the DAC's.
64 /// Matter §6.2.3: a scoped PAI authorises only the matching
65 /// product.
66 #[error("PAI is not authorized for DAC's product")]
67 PaiVidNotAuthorized,
68
69 /// The PAA that anchored the chain is VID-scoped (its subject DN
70 /// carries a [`VendorId`]) but that VID does not equal the DAC/PAI
71 /// subject VID.
72 ///
73 /// Matter Core Spec §6.2.2.1 requires a commissioner to verify that
74 /// a VID-scoped PAA only anchors attestation chains whose DAC and
75 /// PAI subject VID equal the PAA's scoped VID. `rustls-webpki`
76 /// performs only RFC 5280 DN-chaining — it treats the Matter VID
77 /// OID as an opaque DN attribute, not as a `NameConstraint` — so
78 /// without this overlay a VID-scoped PAA could anchor a chain for a
79 /// different vendor. (chip's `DeviceAttestationVerifier` enforces
80 /// the same rule.)
81 #[error("VID-scoped PAA scope mismatch: PAA={paa_vid:?} DAC/PAI={dac_vid:?}")]
82 PaaVidScopeMismatch {
83 /// [`VendorId`] the anchoring PAA is scoped to (its subject DN).
84 paa_vid: VendorId,
85 /// [`VendorId`] observed on the DAC/PAI subject (these two are
86 /// already known equal by the time this check runs).
87 dac_vid: VendorId,
88 },
89
90 /// `attestation_elements` TLV failed to decode or is missing
91 /// required fields (CD bytes, nonce, timestamp).
92 ///
93 /// Returned by
94 /// [`crate::attestation::extract_attestation_elements_fields`] when
95 /// the outer shape is not an anonymous structure, the structure is
96 /// truncated, a required context-tagged field (1 = CD bytes,
97 /// 2 = nonce, 3 = timestamp) is missing or has the wrong wire type,
98 /// the nonce is not exactly 32 bytes, or a required field appears
99 /// more than once.
100 #[error("attestation_elements malformed or missing required fields")]
101 ResponseElementsMalformed,
102
103 /// Certification Declaration (CD) has invalid CMS structure: it
104 /// failed `ContentInfo` / `SignedData` DER parse, declared
105 /// multiple signers, lacked an attached eContent, used an
106 /// unexpected `contentType` / `signatureAlgorithm`, or otherwise
107 /// did not match the Matter Core Spec §6.3.1 shape expected by
108 /// [`crate::attestation::verify_certification_declaration`].
109 #[error("certification declaration has invalid CMS structure")]
110 CertificationDeclarationMalformed,
111
112 /// Certification Declaration signature did not verify against any
113 /// trusted root in the supplied
114 /// [`crate::attestation::CdSigningRoots`] store.
115 #[error("certification declaration signature does not verify against any trusted root")]
116 CertificationDeclarationSignatureInvalid,
117
118 /// Certification Declaration inner TLV (the signed eContent
119 /// payload) is malformed, truncated, or missing a required
120 /// context-tagged field per Matter Core Spec §6.3.1.
121 #[error("certification declaration inner TLV malformed")]
122 CertificationDeclarationTlvMalformed,
123
124 /// Vendor ID declared inside the verified Certification
125 /// Declaration does not equal the VID the caller expected (sourced
126 /// from the verified DAC subject in M6.4.x).
127 #[error(
128 "certification declaration VID mismatch: declared {declared:?}, expected {expected:?}"
129 )]
130 CertificationDeclarationVidMismatch {
131 /// Vendor ID declared inside the CD's inner TLV (tag 1).
132 declared: crate::attestation::VendorId,
133 /// Vendor ID the caller required (typically the DAC subject's VID).
134 expected: crate::attestation::VendorId,
135 },
136
137 /// Product ID list inside the verified Certification Declaration
138 /// does not contain the PID the caller expected.
139 #[error("certification declaration PID list does not contain expected {0:?}")]
140 CertificationDeclarationPidMismatch(crate::attestation::ProductId),
141
142 /// ECDSA verification of the device's attestation-response signature
143 /// over `attestation_elements || attestation_challenge` did not
144 /// succeed against the DAC public key.
145 ///
146 /// **Deliberately coarse.** Per the M6.2 design (§Error handling —
147 /// information leakage table), this variant does NOT distinguish
148 /// between
149 ///
150 /// - signature bytes corrupted in transit,
151 /// - the device signed with a key other than the DAC's,
152 /// - the wrong `attestation_challenge` was supplied (e.g. a
153 /// replay or session-state mismatch), or
154 /// - `attestation_elements` was tampered.
155 ///
156 /// A more granular surface here would let an attacker probe which
157 /// of these failed, narrowing their guess for the actual session
158 /// challenge.
159 #[error("AttestationResponse signature verification failed")]
160 BadResponseSignature,
161}
162
163/// Map a [`webpki::Error`] kind to our typed [`AttestationError`].
164///
165/// Spec-mandated mapping (see the M6.2 design doc, §Error type —
166/// "Well-known [`webpki::Error`] kinds"), adapted to `webpki 0.103`'s
167/// actual variant set:
168///
169/// | [`webpki::Error`] kind | [`AttestationError`] |
170/// |-------------------------------------------------------------------------------------|-------------------------------|
171/// | `CertExpired{..}`, `CertNotValidYet{..}`, `InvalidCertValidity` | `TimeBoundsViolation` |
172/// | `PathLenConstraintViolated`, `EndEntityUsedAsCa`, `CaUsedAsEndEntity` | `BasicConstraintsViolation` |
173/// | `UnknownIssuer` | `UntrustedRoot` |
174/// | (any other kind) | `InvalidChain(boxed)` |
175///
176/// [`AttestationError::BasicConstraintsViolation`] covers three
177/// distinct `BasicConstraints`-extension errors webpki distinguishes:
178/// - `EndEntityUsedAsCa` — a CA-marked cert was relied on as a leaf
179/// (this is what fires when a DAC has `cA = true`, since webpki then
180/// refuses to use it as the end-entity).
181/// - `CaUsedAsEndEntity` — same family of bug from the other direction.
182/// - `PathLenConstraintViolated` — chain too long for the intermediate's
183/// declared `pathLenConstraint`.
184///
185/// All three are `BasicConstraints` extension semantics per RFC 5280
186/// §4.2.1.9, so they fold into our single typed variant.
187//
188// pub(crate) because the only legitimate caller is chain.rs::verify_chain.
189// Directed tests in chain.rs / this file cover every row of the table.
190pub(crate) fn map_webpki_error(err: webpki::Error) -> AttestationError {
191 use webpki::Error as W;
192 match err {
193 W::CertExpired { .. } | W::CertNotValidYet { .. } | W::InvalidCertValidity => {
194 AttestationError::TimeBoundsViolation
195 }
196 W::PathLenConstraintViolated | W::EndEntityUsedAsCa | W::CaUsedAsEndEntity => {
197 AttestationError::BasicConstraintsViolation
198 }
199 W::UnknownIssuer => AttestationError::UntrustedRoot,
200 other => AttestationError::InvalidChain(Box::new(other)),
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use core::time::Duration;
208 use rustls_pki_types::UnixTime;
209
210 fn epoch() -> UnixTime {
211 UnixTime::since_unix_epoch(Duration::from_secs(0))
212 }
213
214 #[test]
215 fn maps_cert_expired_to_time_bounds_violation() {
216 let err = map_webpki_error(webpki::Error::CertExpired {
217 time: epoch(),
218 not_after: epoch(),
219 });
220 assert!(matches!(err, AttestationError::TimeBoundsViolation));
221 }
222
223 #[test]
224 fn maps_cert_not_valid_yet_to_time_bounds_violation() {
225 let err = map_webpki_error(webpki::Error::CertNotValidYet {
226 time: epoch(),
227 not_before: epoch(),
228 });
229 assert!(matches!(err, AttestationError::TimeBoundsViolation));
230 }
231
232 #[test]
233 fn maps_invalid_cert_validity_to_time_bounds_violation() {
234 let err = map_webpki_error(webpki::Error::InvalidCertValidity);
235 assert!(matches!(err, AttestationError::TimeBoundsViolation));
236 }
237
238 #[test]
239 fn maps_path_len_constraint_violated_to_basic_constraints_violation() {
240 let err = map_webpki_error(webpki::Error::PathLenConstraintViolated);
241 assert!(matches!(err, AttestationError::BasicConstraintsViolation));
242 }
243
244 #[test]
245 fn maps_end_entity_used_as_ca_to_basic_constraints_violation() {
246 let err = map_webpki_error(webpki::Error::EndEntityUsedAsCa);
247 assert!(matches!(err, AttestationError::BasicConstraintsViolation));
248 }
249
250 #[test]
251 fn maps_ca_used_as_end_entity_to_basic_constraints_violation() {
252 let err = map_webpki_error(webpki::Error::CaUsedAsEndEntity);
253 assert!(matches!(err, AttestationError::BasicConstraintsViolation));
254 }
255
256 #[test]
257 fn maps_unknown_issuer_to_untrusted_root() {
258 let err = map_webpki_error(webpki::Error::UnknownIssuer);
259 assert!(matches!(err, AttestationError::UntrustedRoot));
260 }
261
262 #[test]
263 fn maps_long_tail_to_invalid_chain() {
264 // Pick a kind that's NOT in the mapping table — signature
265 // failure is a representative member of the "everything else"
266 // bucket.
267 let err = map_webpki_error(webpki::Error::InvalidSignatureForPublicKey);
268 assert!(matches!(err, AttestationError::InvalidChain(_)));
269 }
270
271 #[test]
272 fn bad_response_signature_variant_exists() {
273 // Construction smoke test: this variant must be a unit variant so
274 // it carries no information beyond "verification failed" — see
275 // M6.2 design §Error handling: the single coarse variant prevents
276 // the error channel from leaking which secret (key, challenge,
277 // elements, or signature) was off.
278 let err = AttestationError::BadResponseSignature;
279 assert!(matches!(err, AttestationError::BadResponseSignature));
280 // Display string covers what the operator will see; assert on its
281 // exact text so a future rename breaks a test, not a log.
282 assert_eq!(
283 format!("{err}"),
284 "AttestationResponse signature verification failed"
285 );
286 }
287}