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 value too large for the type this crate represents it in.
47    ValueTooLarge,
48}
49
50impl fmt::Display for ErrorCode {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        match self {
53            Self::Encoding(code) => write!(f, "{code}"),
54            Self::ForeignReader => f.write_str("reader belongs to another parser"),
55            Self::MissingField => f.write_str("required field absent"),
56            Self::UnexpectedField => f.write_str("field not part of this version"),
57            Self::TrailingData => f.write_str("bytes left over"),
58            Self::EmptySequence => f.write_str("sequence with no members"),
59            Self::DuplicateExtension => f.write_str("extension appears more than once"),
60            Self::MalformedTime => f.write_str("malformed time"),
61            Self::TimeOutOfRange => f.write_str("time names a moment that does not exist"),
62            Self::UnsupportedVersion => f.write_str("unknown version"),
63            Self::UnsupportedNameSyntax => f.write_str("unsupported distinguished-name syntax"),
64            Self::AlgorithmMismatch => {
65                f.write_str("signed and unsigned algorithm identifiers differ")
66            }
67            Self::MalformedValue => f.write_str("value the schema does not allow"),
68            Self::ValueTooLarge => f.write_str("value too large to represent"),
69        }
70    }
71}
72
73/// A certificate that could not be read, and where.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct Error {
76    code: ErrorCode,
77    field: &'static str,
78    offset: u64,
79}
80
81impl Error {
82    /// A resource failure at the originating document offset, without allocation.
83    #[must_use]
84    pub fn resource(
85        reason: structured_asn1::memory::MemoryError,
86        field: &'static str,
87        offset: u64,
88    ) -> Self {
89        Self::new(ErrorCode::Encoding(reason.into()), field, offset)
90    }
91    /// An error in a named field, at a known offset in the document.
92    #[must_use]
93    pub fn new(code: ErrorCode, field: &'static str, offset: u64) -> Self {
94        Self {
95            code,
96            field,
97            offset,
98        }
99    }
100
101    /// Why it stopped.
102    #[must_use]
103    pub fn code(&self) -> ErrorCode {
104        self.code
105    }
106
107    /// The field of the schema, outermost first.
108    #[must_use]
109    pub fn field(&self) -> &'static str {
110        self.field
111    }
112
113    /// Where the trouble is, counted in the whole document.
114    #[must_use]
115    pub fn offset(&self) -> u64 {
116        self.offset
117    }
118
119    /// Whether this run declined to finish rather than the certificate being
120    /// wrong. A spent budget says nothing about the document, and reporting it
121    /// as a broken certificate would tell somebody their document is damaged
122    /// when the truth is that this device gave up.
123    #[must_use]
124    pub fn is_limit(&self) -> bool {
125        matches!(
126            self.code,
127            ErrorCode::Encoding(
128                structured_asn1::ErrorCode::LimitExceeded(_)
129                    | structured_asn1::ErrorCode::TagTooLarge
130            )
131        )
132    }
133
134    /// Whether resource exhaustion stopped the run, including allocator failure.
135    #[must_use]
136    pub fn is_resource(&self) -> bool {
137        self.is_limit()
138            || matches!(
139                self.code,
140                ErrorCode::Encoding(
141                    structured_asn1::ErrorCode::AllocatorExhausted
142                        | structured_asn1::ErrorCode::CapacityOverflow
143                )
144            )
145    }
146}
147
148impl fmt::Display for Error {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        write!(f, "{} in {} at {}", self.code, self.field, self.offset)
151    }
152}
153
154#[cfg(feature = "std")]
155impl std::error::Error for Error {}
156
157/// The result of reading one thing.
158pub type Result<T> = core::result::Result<T, Error>;
159
160/// Names the field an encoding error was found in.
161pub(crate) trait Located<T> {
162    /// Turns a reader error into one that says which field was being read.
163    fn field(self, name: &'static str) -> Result<T>;
164}
165
166impl<T> Located<T> for core::result::Result<T, structured_asn1::Error> {
167    fn field(self, name: &'static str) -> Result<T> {
168        self.map_err(|error| Error::new(ErrorCode::Encoding(error.code()), name, error.offset()))
169    }
170}
171
172/// Splits a fallible step inside a yes-or-no gate: a spent budget leaves as
173/// the error it is, because it says the device gave up rather than anything
174/// about the document, while any other failure answers that the value was not
175/// understood.
176pub(crate) fn gate<T>(result: Result<T>) -> Result<Option<T>> {
177    match result {
178        Ok(value) => Ok(Some(value)),
179        Err(error) if error.is_resource() => Err(error),
180        Err(_) => Ok(None),
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::{Error, ErrorCode, gate};
187
188    #[test]
189    fn a_tag_header_ceiling_survives_optional_field_probing() {
190        // Refusing a wide header cannot make an optional value disappear.
191        let error = Error::new(
192            ErrorCode::Encoding(structured_asn1::ErrorCode::TagTooLarge),
193            "extension",
194            42,
195        );
196        assert!(error.is_limit());
197        let result: super::Result<Option<()>> = gate(Err(error.clone()));
198        assert_eq!(result.unwrap_err(), error);
199    }
200
201    #[test]
202    fn allocator_failure_is_not_a_document_fault() {
203        // Optional-field probing must propagate resource failures rather than
204        // classify the field as absent or malformed when storage is unavailable.
205        let error = Error::new(
206            ErrorCode::Encoding(structured_asn1::ErrorCode::AllocatorExhausted),
207            "extension",
208            42,
209        );
210        let result: super::Result<Option<()>> = gate(Err(error.clone()));
211        assert_eq!(result.unwrap_err(), error);
212    }
213}