Skip to main content

structured_pkix/
error.rs

1//! What was wrong with a certificate, and which field it was in.
2//!
3//! The reader below this one reports a path of element indices, because it does
4//! not know the schema. This crate does know it, so it reports the field by
5//! name: `tbsCertificate.validity.notAfter` says the same thing as `/0/4/1` and
6//! says it to a person. Neither carries bytes from the document.
7
8use core::fmt;
9
10mod callback;
11pub use callback::{CallbackError, CallbackResult};
12
13/// Why a certificate could not be read.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum ErrorCode {
17    /// The bytes are not the encoding the schema asks for here.
18    Encoding(structured_asn1::ErrorCode),
19    /// The caller supplied another parser for a producing-reader operation.
20    ForeignReader,
21    /// A field the schema requires is absent.
22    MissingField,
23    /// A field is present that this version of the structure does not have.
24    UnexpectedField,
25    /// Bytes after the end of a structure that should have used all of them.
26    TrailingData,
27    /// A sequence that must hold at least one member holds none.
28    EmptySequence,
29    /// The same extension appears more than once, leaving no answer to which
30    /// of them applies.
31    DuplicateExtension,
32    /// A time that is not the fixed-width form with seconds and `Z` that
33    /// certificates are required to use.
34    MalformedTime,
35    /// A time whose fields name a moment that does not exist.
36    TimeOutOfRange,
37    /// A version number this crate does not know.
38    UnsupportedVersion,
39    /// An RFC 4514 attribute requires unavailable syntax or equality semantics.
40    UnsupportedNameSyntax,
41    /// The algorithm named outside the signed bytes is not the one named
42    /// inside them.
43    AlgorithmMismatch,
44    /// A value the schema constrains further than its encoding does.
45    MalformedValue,
46    /// A DER component explicitly writes its ASN.1 DEFAULT value.
47    DefaultWritten,
48    /// A value too large for the type this crate represents it in.
49    ValueTooLarge,
50}
51
52impl fmt::Display for ErrorCode {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            Self::Encoding(code) => write!(f, "{code}"),
56            Self::ForeignReader => f.write_str("reader belongs to another parser"),
57            Self::MissingField => f.write_str("required field absent"),
58            Self::UnexpectedField => f.write_str("field not part of this version"),
59            Self::TrailingData => f.write_str("bytes left over"),
60            Self::EmptySequence => f.write_str("sequence with no members"),
61            Self::DuplicateExtension => f.write_str("extension appears more than once"),
62            Self::MalformedTime => f.write_str("malformed time"),
63            Self::TimeOutOfRange => f.write_str("time names a moment that does not exist"),
64            Self::UnsupportedVersion => f.write_str("unknown version"),
65            Self::UnsupportedNameSyntax => f.write_str("unsupported distinguished-name syntax"),
66            Self::AlgorithmMismatch => {
67                f.write_str("signed and unsigned algorithm identifiers differ")
68            }
69            Self::MalformedValue => f.write_str("value the schema does not allow"),
70            Self::DefaultWritten => f.write_str("default value explicitly encoded"),
71            Self::ValueTooLarge => f.write_str("value too large to represent"),
72        }
73    }
74}
75
76/// A certificate that could not be read, and where.
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct Error {
79    code: ErrorCode,
80    field: &'static str,
81    offset: u64,
82}
83
84impl Error {
85    /// A resource failure at the originating document offset, without allocation.
86    #[must_use]
87    pub fn resource(
88        reason: structured_asn1::memory::MemoryError,
89        field: &'static str,
90        offset: u64,
91    ) -> Self {
92        Self::new(ErrorCode::Encoding(reason.into()), field, offset)
93    }
94    /// An error in a named field, at a known offset in the document.
95    #[must_use]
96    pub fn new(code: ErrorCode, field: &'static str, offset: u64) -> Self {
97        Self {
98            code,
99            field,
100            offset,
101        }
102    }
103
104    /// Why it stopped.
105    #[must_use]
106    pub fn code(&self) -> ErrorCode {
107        self.code
108    }
109
110    /// The field of the schema, outermost first.
111    #[must_use]
112    pub fn field(&self) -> &'static str {
113        self.field
114    }
115
116    /// Where the trouble is, counted in the whole document.
117    #[must_use]
118    pub fn offset(&self) -> u64 {
119        self.offset
120    }
121
122    /// Whether this run declined to finish rather than the certificate being
123    /// wrong. A spent budget says nothing about the document, and reporting it
124    /// as a broken certificate would tell somebody their document is damaged
125    /// when the truth is that this device gave up.
126    #[must_use]
127    pub fn is_limit(&self) -> bool {
128        matches!(
129            self.code,
130            ErrorCode::Encoding(
131                structured_asn1::ErrorCode::LimitExceeded(_)
132                    | structured_asn1::ErrorCode::TagTooLarge
133            )
134        )
135    }
136
137    /// Whether resource exhaustion stopped the run, including allocator failure.
138    #[must_use]
139    pub fn is_resource(&self) -> bool {
140        self.is_limit()
141            || matches!(
142                self.code,
143                ErrorCode::Encoding(
144                    structured_asn1::ErrorCode::AllocatorExhausted
145                        | structured_asn1::ErrorCode::CapacityOverflow
146                )
147            )
148    }
149}
150
151impl fmt::Display for Error {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        write!(f, "{} in {} at {}", self.code, self.field, self.offset)
154    }
155}
156
157#[cfg(feature = "std")]
158impl std::error::Error for Error {}
159
160/// The result of reading one thing.
161pub type Result<T> = core::result::Result<T, Error>;
162
163/// Names the field an encoding error was found in.
164pub(crate) trait Located<T> {
165    /// Turns a reader error into one that says which field was being read.
166    fn field(self, name: &'static str) -> Result<T>;
167}
168
169impl<T> Located<T> for core::result::Result<T, structured_asn1::Error> {
170    fn field(self, name: &'static str) -> Result<T> {
171        self.map_err(|error| Error::new(ErrorCode::Encoding(error.code()), name, error.offset()))
172    }
173}
174
175/// Splits a fallible step inside a yes-or-no gate: a spent budget leaves as
176/// the error it is, because it says the device gave up rather than anything
177/// about the document, while any other failure answers that the value was not
178/// understood.
179pub(crate) fn gate<T>(result: Result<T>) -> Result<Option<T>> {
180    match result {
181        Ok(value) => Ok(Some(value)),
182        Err(error) if error.is_resource() => Err(error),
183        Err(_) => Ok(None),
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::{Error, ErrorCode, gate};
190
191    #[test]
192    fn a_tag_header_ceiling_survives_optional_field_probing() {
193        // Refusing a wide header cannot make an optional value disappear.
194        let error = Error::new(
195            ErrorCode::Encoding(structured_asn1::ErrorCode::TagTooLarge),
196            "extension",
197            42,
198        );
199        assert!(error.is_limit());
200        let result: super::Result<Option<()>> = gate(Err(error.clone()));
201        assert_eq!(result.unwrap_err(), error);
202    }
203
204    #[test]
205    fn allocator_failure_is_not_a_document_fault() {
206        // Optional-field probing must propagate resource failures rather than
207        // classify the field as absent or malformed when storage is unavailable.
208        let error = Error::new(
209            ErrorCode::Encoding(structured_asn1::ErrorCode::AllocatorExhausted),
210            "extension",
211            42,
212        );
213        let result: super::Result<Option<()>> = gate(Err(error.clone()));
214        assert_eq!(result.unwrap_err(), error);
215    }
216}