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    /// 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}