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