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