Skip to main content

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    /// The certificate breaches the Matter attestation certificate
48    /// *profile* (Matter Core Spec §6.2.2) in a way other than
49    /// `BasicConstraints`: a wrong version or signature algorithm, or a
50    /// `KeyUsage` / `SubjectKeyIdentifier` / `AuthorityKeyIdentifier`
51    /// extension that is absent, mis-flagged (wrong criticality or
52    /// bits), duplicated, or malformed. Mirrors connectedhomeip's
53    /// `VerifyAttestationCertificateFormat`; `rustls-webpki` enforces
54    /// none of these (it ignores `KeyUsage` and never requires
55    /// SKID/AKID), so this check runs in our own code as a peer of
56    /// [`crate::attestation::verify_chain`]. (`BasicConstraints`
57    /// breaches surface as [`AttestationError::BasicConstraintsViolation`].)
58    #[error("attestation certificate format violation: {reason}")]
59    CertFormatViolation {
60        /// Human-readable description of which profile rule failed.
61        reason: &'static str,
62    },
63
64    /// No PAA in the supplied [`crate::attestation::PaaTrustStore`]
65    /// matched the PAI's issuer.
66    #[error("PAA not in trust store")]
67    UntrustedRoot,
68
69    /// DAC subject [`VendorId`] did not equal PAI subject [`VendorId`]
70    /// (Matter §6.2.3 requires equality).
71    #[error("VID mismatch: DAC={dac:?} PAI={pai:?}")]
72    VidMismatch {
73        /// [`VendorId`] observed on the DAC subject.
74        dac: VendorId,
75        /// [`VendorId`] observed on the PAI subject.
76        pai: VendorId,
77    },
78
79    /// PAI is product-scoped (`subject_pid` is `Some`) and its
80    /// [`crate::attestation::ProductId`] differs from the DAC's.
81    /// Matter §6.2.3: a scoped PAI authorises only the matching
82    /// product.
83    #[error("PAI is not authorized for DAC's product")]
84    PaiVidNotAuthorized,
85
86    /// The PAA that anchored the chain is VID-scoped (its subject DN
87    /// carries a [`VendorId`]) but that VID does not equal the DAC/PAI
88    /// subject VID.
89    ///
90    /// Matter Core Spec §6.2.2.1 requires a commissioner to verify that
91    /// a VID-scoped PAA only anchors attestation chains whose DAC and
92    /// PAI subject VID equal the PAA's scoped VID. `rustls-webpki`
93    /// performs only RFC 5280 DN-chaining — it treats the Matter VID
94    /// OID as an opaque DN attribute, not as a `NameConstraint` — so
95    /// without this overlay a VID-scoped PAA could anchor a chain for a
96    /// different vendor. (chip's `DeviceAttestationVerifier` enforces
97    /// the same rule.)
98    #[error("VID-scoped PAA scope mismatch: PAA={paa_vid:?} DAC/PAI={dac_vid:?}")]
99    PaaVidScopeMismatch {
100        /// [`VendorId`] the anchoring PAA is scoped to (its subject DN).
101        paa_vid: VendorId,
102        /// [`VendorId`] observed on the DAC/PAI subject (these two are
103        /// already known equal by the time this check runs).
104        dac_vid: VendorId,
105    },
106
107    /// `attestation_elements` TLV failed to decode or is missing
108    /// required fields (CD bytes, nonce, timestamp).
109    ///
110    /// Returned by
111    /// [`crate::attestation::extract_attestation_elements_fields`] when
112    /// the outer shape is not an anonymous structure, the structure is
113    /// truncated, a required context-tagged field (1 = CD bytes,
114    /// 2 = nonce, 3 = timestamp) is missing or has the wrong wire type,
115    /// the nonce is not exactly 32 bytes, or a required field appears
116    /// more than once.
117    #[error("attestation_elements malformed or missing required fields")]
118    ResponseElementsMalformed,
119
120    /// Certification Declaration (CD) has invalid CMS structure: it
121    /// failed `ContentInfo` / `SignedData` DER parse, declared
122    /// multiple signers, lacked an attached eContent, used an
123    /// unexpected `contentType` / `signatureAlgorithm`, or otherwise
124    /// did not match the Matter Core Spec §6.3.1 shape expected by
125    /// [`crate::attestation::verify_certification_declaration`].
126    #[error("certification declaration has invalid CMS structure")]
127    CertificationDeclarationMalformed,
128
129    /// Certification Declaration signature did not verify against any
130    /// trusted root in the supplied
131    /// [`crate::attestation::CdSigningRoots`] store.
132    #[error("certification declaration signature does not verify against any trusted root")]
133    CertificationDeclarationSignatureInvalid,
134
135    /// Certification Declaration inner TLV (the signed eContent
136    /// payload) is malformed, truncated, or missing a required
137    /// context-tagged field per Matter Core Spec §6.3.1.
138    #[error("certification declaration inner TLV malformed")]
139    CertificationDeclarationTlvMalformed,
140
141    /// Vendor ID declared inside the verified Certification
142    /// Declaration does not equal the VID the caller expected (sourced
143    /// from the verified DAC subject in M6.4.x).
144    #[error(
145        "certification declaration VID mismatch: declared {declared:?}, expected {expected:?}"
146    )]
147    CertificationDeclarationVidMismatch {
148        /// Vendor ID declared inside the CD's inner TLV (tag 1).
149        declared: crate::attestation::VendorId,
150        /// Vendor ID the caller required (typically the DAC subject's VID).
151        expected: crate::attestation::VendorId,
152    },
153
154    /// Product ID list inside the verified Certification Declaration
155    /// does not contain the PID the caller expected.
156    #[error("certification declaration PID list does not contain expected {0:?}")]
157    CertificationDeclarationPidMismatch(crate::attestation::ProductId),
158
159    /// The Certification Declaration carries an `authorized_paa_list`
160    /// (Matter Core Spec §6.3.1 tag 11) that does not include the
161    /// `SubjectKeyIdentifier` of the PAA that anchored the device's DAC
162    /// chain. Per Matter §6.2.3 the device may only be attested under a
163    /// PAA the CD authorizes, so this is a counterfeit-detection reject
164    /// (chip `DefaultDeviceAttestationVerifier.cpp:738`). Also raised when
165    /// the CD scopes its PAAs but the anchoring PAA carries no SKID to
166    /// match.
167    #[error("certification declaration authorized_paa_list does not include the device's PAA")]
168    CertificationDeclarationPaaNotAuthorized,
169
170    /// ECDSA verification of the device's attestation-response signature
171    /// over `attestation_elements || attestation_challenge` did not
172    /// succeed against the DAC public key.
173    ///
174    /// **Deliberately coarse.** Per the M6.2 design (§Error handling —
175    /// information leakage table), this variant does NOT distinguish
176    /// between
177    ///
178    /// - signature bytes corrupted in transit,
179    /// - the device signed with a key other than the DAC's,
180    /// - the wrong `attestation_challenge` was supplied (e.g. a
181    ///   replay or session-state mismatch), or
182    /// - `attestation_elements` was tampered.
183    ///
184    /// A more granular surface here would let an attacker probe which
185    /// of these failed, narrowing their guess for the actual session
186    /// challenge.
187    #[error("AttestationResponse signature verification failed")]
188    BadResponseSignature,
189}
190
191/// Map a [`webpki::Error`] kind to our typed [`AttestationError`].
192///
193/// Spec-mandated mapping (see the M6.2 design doc, §Error type —
194/// "Well-known [`webpki::Error`] kinds"), adapted to `webpki 0.103`'s
195/// actual variant set:
196///
197/// | [`webpki::Error`] kind                                                              | [`AttestationError`]          |
198/// |-------------------------------------------------------------------------------------|-------------------------------|
199/// | `CertExpired{..}`, `CertNotValidYet{..}`, `InvalidCertValidity`                     | `TimeBoundsViolation`         |
200/// | `PathLenConstraintViolated`, `EndEntityUsedAsCa`, `CaUsedAsEndEntity`               | `BasicConstraintsViolation`   |
201/// | `UnknownIssuer`                                                                     | `UntrustedRoot`               |
202/// | (any other kind)                                                                    | `InvalidChain(boxed)`         |
203///
204/// [`AttestationError::BasicConstraintsViolation`] covers three
205/// distinct `BasicConstraints`-extension errors webpki distinguishes:
206/// - `EndEntityUsedAsCa` — a CA-marked cert was relied on as a leaf
207///   (this is what fires when a DAC has `cA = true`, since webpki then
208///   refuses to use it as the end-entity).
209/// - `CaUsedAsEndEntity` — same family of bug from the other direction.
210/// - `PathLenConstraintViolated` — chain too long for the intermediate's
211///   declared `pathLenConstraint`.
212///
213/// All three are `BasicConstraints` extension semantics per RFC 5280
214/// §4.2.1.9, so they fold into our single typed variant.
215//
216// pub(crate) because the only legitimate caller is chain.rs::verify_chain.
217// Directed tests in chain.rs / this file cover every row of the table.
218pub(crate) fn map_webpki_error(err: webpki::Error) -> AttestationError {
219    use webpki::Error as W;
220    match err {
221        W::CertExpired { .. } | W::CertNotValidYet { .. } | W::InvalidCertValidity => {
222            AttestationError::TimeBoundsViolation
223        }
224        W::PathLenConstraintViolated | W::EndEntityUsedAsCa | W::CaUsedAsEndEntity => {
225            AttestationError::BasicConstraintsViolation
226        }
227        W::UnknownIssuer => AttestationError::UntrustedRoot,
228        other => AttestationError::InvalidChain(Box::new(other)),
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235    use core::time::Duration;
236    use rustls_pki_types::UnixTime;
237
238    fn epoch() -> UnixTime {
239        UnixTime::since_unix_epoch(Duration::from_secs(0))
240    }
241
242    #[test]
243    fn maps_cert_expired_to_time_bounds_violation() {
244        let err = map_webpki_error(webpki::Error::CertExpired {
245            time: epoch(),
246            not_after: epoch(),
247        });
248        assert!(matches!(err, AttestationError::TimeBoundsViolation));
249    }
250
251    #[test]
252    fn maps_cert_not_valid_yet_to_time_bounds_violation() {
253        let err = map_webpki_error(webpki::Error::CertNotValidYet {
254            time: epoch(),
255            not_before: epoch(),
256        });
257        assert!(matches!(err, AttestationError::TimeBoundsViolation));
258    }
259
260    #[test]
261    fn maps_invalid_cert_validity_to_time_bounds_violation() {
262        let err = map_webpki_error(webpki::Error::InvalidCertValidity);
263        assert!(matches!(err, AttestationError::TimeBoundsViolation));
264    }
265
266    #[test]
267    fn maps_path_len_constraint_violated_to_basic_constraints_violation() {
268        let err = map_webpki_error(webpki::Error::PathLenConstraintViolated);
269        assert!(matches!(err, AttestationError::BasicConstraintsViolation));
270    }
271
272    #[test]
273    fn maps_end_entity_used_as_ca_to_basic_constraints_violation() {
274        let err = map_webpki_error(webpki::Error::EndEntityUsedAsCa);
275        assert!(matches!(err, AttestationError::BasicConstraintsViolation));
276    }
277
278    #[test]
279    fn maps_ca_used_as_end_entity_to_basic_constraints_violation() {
280        let err = map_webpki_error(webpki::Error::CaUsedAsEndEntity);
281        assert!(matches!(err, AttestationError::BasicConstraintsViolation));
282    }
283
284    #[test]
285    fn maps_unknown_issuer_to_untrusted_root() {
286        let err = map_webpki_error(webpki::Error::UnknownIssuer);
287        assert!(matches!(err, AttestationError::UntrustedRoot));
288    }
289
290    #[test]
291    fn maps_long_tail_to_invalid_chain() {
292        // Pick a kind that's NOT in the mapping table — signature
293        // failure is a representative member of the "everything else"
294        // bucket.
295        let err = map_webpki_error(webpki::Error::InvalidSignatureForPublicKey);
296        assert!(matches!(err, AttestationError::InvalidChain(_)));
297    }
298
299    #[test]
300    fn bad_response_signature_variant_exists() {
301        // Construction smoke test: this variant must be a unit variant so
302        // it carries no information beyond "verification failed" — see
303        // M6.2 design §Error handling: the single coarse variant prevents
304        // the error channel from leaking which secret (key, challenge,
305        // elements, or signature) was off.
306        let err = AttestationError::BadResponseSignature;
307        assert!(matches!(err, AttestationError::BadResponseSignature));
308        // Display string covers what the operator will see; assert on its
309        // exact text so a future rename breaks a test, not a log.
310        assert_eq!(
311            format!("{err}"),
312            "AttestationResponse signature verification failed"
313        );
314    }
315}