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