Skip to main content

structured_cms/
directory.rs

1//! Accounted certificate hint indexes, without signer identity or trust verdicts.
2
3use crate::Error as ProcessingError;
4use crate::signer::SignerIdentifier;
5use core::cmp::Ordering;
6use core::fmt;
7use structured_asn1::Reader;
8use structured_asn1::memory::{Execution, Memory, MemoryError, OwnedVec, SortError, Work};
9use structured_pkix::{Certificate, Name, Ordered};
10
11/// A directory operation has either an original document cause or a storage/
12/// metadata admission failure that has no document position of its own.
13#[derive(Debug)]
14pub enum Error {
15    /// Original certificate, name or signer-hint processing failure.
16    Processing(ProcessingError),
17    /// Source-free retained catalogue work or storage refusal.
18    Resource(MemoryError),
19}
20
21impl Error {
22    fn pkix(source: structured_pkix::Error) -> Self {
23        Self::Processing(ProcessingError::pkix(source))
24    }
25    fn resource(reason: MemoryError, field: &'static str, at: u64) -> Self {
26        Self::Processing(ProcessingError::resource(reason, field, at))
27    }
28    fn admission(reason: MemoryError, origin: Option<u64>) -> Self {
29        match origin {
30            Some(at) => Self::resource(reason, "signedData.certificates", at),
31            None => Self::Resource(reason),
32        }
33    }
34}
35
36impl fmt::Display for Error {
37    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38        match self {
39            Self::Processing(source) => source.fmt(formatter),
40            Self::Resource(source) => source.fmt(formatter),
41        }
42    }
43}
44
45impl core::error::Error for Error {
46    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
47        match self {
48            Self::Processing(source) => Some(source),
49            Self::Resource(source) => Some(source),
50        }
51    }
52}
53
54/// A complete catalogue operation; refusal never becomes a match answer.
55pub type Result<T> = core::result::Result<T, Error>;
56
57/// What the complete catalogue can establish about one certificate hint.
58/// Matching a hint never authenticates that certificate or its path.
59#[derive(Debug)]
60pub enum Match<'c, 'a> {
61    /// No certificate answers the complete hint, with no unreadable SKI gap.
62    None,
63    /// Exactly one certificate answers the complete hint.
64    One(&'c Certificate<'a>),
65    /// More than one certificate answers the complete hint; no candidate chosen.
66    Ambiguous,
67    /// An SKI value could not be read, so uniqueness/absence is unestablished.
68    /// Original diagnostics are retained by [`Directory::unreadable_identifiers`].
69    Unreadable,
70}
71
72/// One caller-selected certificate's hint agreement, without uniqueness/trust.
73#[derive(Debug)]
74pub enum CertificateAgreement {
75    /// The original issuer/serial or SKI hint agrees.
76    Matches,
77    /// The complete readable hint names something else.
78    Differs,
79    /// Original certificate SKI syntax prevents a comparison.
80    Unreadable(structured_pkix::Error),
81}
82
83/// Compares a selected certificate without constructing an issuer directory.
84///
85/// This uses the same paid byte/segmented and shared Name kernels as directory
86/// search. Original certificate parsing may be a different session; lazy SKI
87/// keeps that origin and also obeys the current query operation. This establishes
88/// neither uniqueness in a pool nor signature/path trust for the selected key.
89pub fn matches_certificate_in(
90    named: &SignerIdentifier<'_>,
91    certificate: &Certificate<'_>,
92    parent: &Reader<'_>,
93) -> Result<CertificateAgreement> {
94    let execution = parent.execution();
95    let at = certificate.span().start;
96    match named {
97        SignerIdentifier::IssuerAndSerial { issuer, serial } => {
98            if bytes_order(
99                serial.as_bytes(),
100                certificate.tbs().serial().as_bytes(),
101                &execution,
102                at,
103            )? != Ordering::Equal
104            {
105                return Ok(CertificateAgreement::Differs);
106            }
107            if issuer
108                .matches_in(certificate.tbs().issuer(), parent)
109                .map_err(Error::pkix)?
110            {
111                Ok(CertificateAgreement::Matches)
112            } else {
113                Ok(CertificateAgreement::Differs)
114            }
115        }
116        SignerIdentifier::SubjectKeyIdentifier(wanted) => {
117            let Some(extension) = certificate
118                .tbs()
119                .extensions()
120                .get(structured_pkix::oid::SUBJECT_KEY_IDENTIFIER, &execution)
121                .map_err(Error::pkix)?
122            else {
123                return Ok(CertificateAgreement::Differs);
124            };
125            let identifier =
126                match structured_pkix::identifier::SubjectKeyIdentifier::read_in(extension, parent)
127                {
128                    Ok(identifier) => identifier,
129                    Err(error)
130                        if error.is_resource()
131                            || error.code() == structured_pkix::ErrorCode::ForeignReader =>
132                    {
133                        return Err(Error::pkix(error));
134                    }
135                    Err(error) => return Ok(CertificateAgreement::Unreadable(error)),
136                };
137            if segmented_matches(wanted, identifier.as_bytes(), &execution, at)? {
138                Ok(CertificateAgreement::Matches)
139            } else {
140                Ok(CertificateAgreement::Differs)
141            }
142        }
143    }
144}
145
146/// Original malformed identifier diagnostic, independently of resource failures.
147#[derive(Debug)]
148pub struct Unreadable<'c, 'a> {
149    certificate: &'c Certificate<'a>,
150    reason: structured_pkix::Error,
151}
152impl<'c, 'a> Unreadable<'c, 'a> {
153    /// Certificate whose actual extension was unreadable.
154    #[must_use]
155    pub fn certificate(&self) -> &'c Certificate<'a> {
156        self.certificate
157    }
158    /// Original located encoding/schema cause, without conversion to absence.
159    #[must_use]
160    pub fn reason(&self) -> &structured_pkix::Error {
161        &self.reason
162    }
163}
164
165struct Identifier<'a> {
166    bytes: &'a [u8],
167    index: usize,
168}
169
170/// Prepared issuer/serial and SKI indexes retaining their own capacity charges.
171///
172/// Names are prepared once per certificate, not inside sorting comparators.
173/// Owner type determines storage/execution lifetime, never semantic ordering.
174/// Every new lookup explicitly funds its current domain. Unknown/malformed SKI
175/// gaps cannot become a completed None/One result; resource refusal aborts.
176pub struct Directory<'c, 'a, W: Work = Memory> {
177    certificates: OwnedVec<&'c Certificate<'a>, W>,
178    issuers: OwnedVec<Ordered<'a, W>, W>,
179    serials: OwnedVec<usize, W>,
180    identifiers: OwnedVec<Identifier<'a>, W>,
181    unreadable: OwnedVec<Unreadable<'c, 'a>, W>,
182}
183
184impl<'c, 'a> Directory<'c, 'a> {
185    /// Independently retained directory over certificates from any source session.
186    /// Current Memory funds all construction; deferred SKI parsing preserves
187    /// each original certificate's reader/depth and its cumulative parse ceiling.
188    pub fn of(certificates: &[&'c Certificate<'a>], execution: &Memory) -> Result<Self> {
189        let mut kept = execution.vector();
190        kept.try_extend_from_slice(certificates)
191            .map_err(Error::Resource)?;
192        build(
193            kept,
194            execution,
195            None,
196            |name| name.ordered(execution),
197            |extension| {
198                structured_pkix::identifier::SubjectKeyIdentifier::read(extension, execution)
199            },
200        )
201    }
202}
203
204impl<'c, 'a, 'p> Directory<'c, 'a, Execution<'p>> {
205    /// Scoped directory over carried certificates from this one parser session.
206    ///
207    /// The source identity of every certificate is checked before allocation,
208    /// normalization or lazy SKI parsing. A foreign pool belongs in [`Directory::of`];
209    /// account equality cannot make it part of the producing document session.
210    pub fn of_in(certificates: &[&'c Certificate<'a>], parent: &'p Reader<'_>) -> Result<Self>
211    where
212        'a: 'p,
213    {
214        let execution = parent.execution();
215        // Preflight the borrowed catalogue before any retained storage or
216        // deferred processing. One handle copy then feeds the shared builder.
217        for certificate in certificates {
218            execution.charge_work(1).map_err(|reason| {
219                Error::resource(reason, "signedData.certificates", parent.offset())
220            })?;
221            if !certificate
222                .tbs()
223                .public_key()
224                .algorithm()
225                .same_session(parent)
226            {
227                return Err(Error::pkix(structured_pkix::Error::new(
228                    structured_pkix::ErrorCode::ForeignReader,
229                    "signedData.certificates",
230                    certificate.span().start,
231                )));
232            }
233        }
234        let mut gathered = execution.vector();
235        gathered
236            .try_extend_from_slice(certificates)
237            .map_err(|reason| Error::admission(reason, Some(parent.offset())))?;
238        build(
239            gathered,
240            &execution,
241            Some(parent.offset()),
242            |name| name.ordered_in(parent),
243            |extension| {
244                structured_pkix::identifier::SubjectKeyIdentifier::read_in(extension, parent)
245            },
246        )
247    }
248}
249
250impl<'c, 'a, W: Work> Directory<'c, 'a, W> {
251    /// Indexed certificate count, without rescanning.
252    #[must_use]
253    pub fn len(&self) -> usize {
254        self.certificates.len()
255    }
256    /// Whether this directory carries no certificate hints.
257    #[must_use]
258    pub fn is_empty(&self) -> bool {
259        self.certificates.is_empty()
260    }
261    /// Original malformed SKI diagnostics, with their actual certificates.
262    #[must_use]
263    pub fn unreadable_identifiers(&self) -> &[Unreadable<'c, 'a>] {
264        &self.unreadable
265    }
266
267    /// Retained query using current aggregate funding, independent of storage owner.
268    pub fn find(&self, named: &SignerIdentifier<'_>, execution: &Memory) -> Result<Match<'c, 'a>> {
269        self.find_with(named, execution, None, |name| name.ordered(execution))
270    }
271    /// Producing query using both current local and aggregate ceilings.
272    /// A retained pool and scoped pool use the same index/search kernel.
273    pub fn find_in(
274        &self,
275        named: &SignerIdentifier<'_>,
276        parent: &Reader<'_>,
277    ) -> Result<Match<'c, 'a>> {
278        self.find_with(named, &parent.execution(), Some(parent.offset()), |name| {
279            name.ordered_in(parent)
280        })
281    }
282
283    fn find_with<'n, V: Work, E: Work>(
284        &self,
285        named: &SignerIdentifier<'n>,
286        execution: &E,
287        origin: Option<u64>,
288        prepare: impl FnOnce(&Name<'n>) -> structured_pkix::Result<Ordered<'n, V>>,
289    ) -> Result<Match<'c, 'a>> {
290        match named {
291            SignerIdentifier::IssuerAndSerial { issuer, serial } => {
292                let asked = prepare(issuer).map_err(Error::pkix)?;
293                let against = |index: usize| -> Result<Ordering> {
294                    let held = self.certificates[index].tbs().serial().as_bytes();
295                    let order = bytes_order(
296                        held,
297                        serial.as_bytes(),
298                        execution,
299                        self.certificates[index].span().start,
300                    )?;
301                    if order != Ordering::Equal {
302                        return Ok(order);
303                    }
304                    self.issuers[index]
305                        .order(&asked, execution)
306                        .map_err(Error::pkix)
307                };
308                let from = lower_bound(self.serials.len(), execution, origin, |at| {
309                    against(self.serials[at])
310                })?;
311                let mut first = None;
312                for &index in &self.serials[from..] {
313                    if against(index)? != Ordering::Equal {
314                        break;
315                    }
316                    if first.is_some() {
317                        return Ok(Match::Ambiguous);
318                    }
319                    first = Some(self.certificates[index]);
320                }
321                Ok(match first {
322                    Some(certificate) => Match::One(certificate),
323                    None => Match::None,
324                })
325            }
326            SignerIdentifier::SubjectKeyIdentifier(wanted) => {
327                let mut first = None;
328                if let Some(bytes) = wanted.as_one() {
329                    let from = lower_bound(self.identifiers.len(), execution, origin, |at| {
330                        bytes_order(
331                            self.identifiers[at].bytes,
332                            bytes,
333                            execution,
334                            self.certificates[self.identifiers[at].index].span().start,
335                        )
336                    })?;
337                    for held in &self.identifiers[from..] {
338                        if bytes_order(
339                            held.bytes,
340                            bytes,
341                            execution,
342                            self.certificates[held.index].span().start,
343                        )? != Ordering::Equal
344                        {
345                            break;
346                        }
347                        if first.is_some() {
348                            return Ok(Match::Ambiguous);
349                        }
350                        first = Some(self.certificates[held.index]);
351                    }
352                } else {
353                    // Compare the original segmented identifier without joining.
354                    for held in &self.identifiers {
355                        let at = self.certificates[held.index].span().start;
356                        execution
357                            .charge_work(1)
358                            .map_err(|reason| Error::resource(reason, "signerInfo.sid", at))?;
359                        if segmented_matches(wanted, held.bytes, execution, at)? {
360                            if first.is_some() {
361                                return Ok(Match::Ambiguous);
362                            }
363                            first = Some(self.certificates[held.index]);
364                        }
365                    }
366                }
367                if !self.unreadable.is_empty() {
368                    return Ok(Match::Unreadable);
369                }
370                Ok(match first {
371                    Some(certificate) => Match::One(certificate),
372                    None => Match::None,
373                })
374            }
375        }
376    }
377}
378
379fn build<'c, 'a, W: Work>(
380    kept: OwnedVec<&'c Certificate<'a>, W>,
381    execution: &W,
382    origin: Option<u64>,
383    mut prepare: impl FnMut(&Name<'a>) -> structured_pkix::Result<Ordered<'a, W>>,
384    mut read_identifier: impl FnMut(
385        &structured_pkix::Extension<'a>,
386    ) -> structured_pkix::Result<
387        structured_pkix::identifier::SubjectKeyIdentifier<'a>,
388    >,
389) -> Result<Directory<'c, 'a, W>> {
390    let mut issuers = execution.vector();
391    let mut serials = execution.vector();
392    let mut identifiers = execution.vector();
393    let mut unreadable = execution.vector();
394    for (index, certificate) in kept.iter().enumerate() {
395        let at = certificate.span().start;
396        execution
397            .charge_work(1)
398            .map_err(|reason| Error::resource(reason, "signedData.certificates", at))?;
399        issuers
400            .try_push(prepare(certificate.tbs().issuer()).map_err(Error::pkix)?)
401            .map_err(|reason| Error::resource(reason, "signedData.certificates", at))?;
402        serials
403            .try_push(index)
404            .map_err(|reason| Error::resource(reason, "signedData.certificates", at))?;
405        let extension = certificate
406            .tbs()
407            .extensions()
408            .get(structured_pkix::oid::SUBJECT_KEY_IDENTIFIER, execution)
409            .map_err(Error::pkix)?;
410        if let Some(extension) = extension {
411            match read_identifier(extension) {
412                Ok(identifier) => identifiers
413                    .try_push(Identifier {
414                        bytes: identifier.as_bytes(),
415                        index,
416                    })
417                    .map_err(|reason| {
418                        Error::resource(
419                            reason,
420                            "extensions.subjectKeyIdentifier",
421                            extension.span().start,
422                        )
423                    })?,
424                Err(reason) if reason.is_resource() => return Err(Error::pkix(reason)),
425                Err(reason) => unreadable
426                    .try_push(Unreadable {
427                        certificate,
428                        reason,
429                    })
430                    .map_err(|reason| {
431                        Error::resource(
432                            reason,
433                            "extensions.subjectKeyIdentifier",
434                            extension.span().start,
435                        )
436                    })?,
437            }
438        }
439    }
440    serials
441        .try_sort_unstable_by(|left, right| {
442            let order = bytes_order(
443                kept[*left].tbs().serial().as_bytes(),
444                kept[*right].tbs().serial().as_bytes(),
445                execution,
446                kept[*left].span().start,
447            )?;
448            if order != Ordering::Equal {
449                return Ok(order);
450            }
451            issuers[*left]
452                .order(&issuers[*right], execution)
453                .map_err(Error::pkix)
454        })
455        .map_err(|error| sort_error(error, origin))?;
456    identifiers
457        .try_sort_unstable_by(|left, right| {
458            bytes_order(
459                left.bytes,
460                right.bytes,
461                execution,
462                kept[left.index].span().start,
463            )
464        })
465        .map_err(|error| sort_error(error, origin))?;
466    Ok(Directory {
467        certificates: kept,
468        issuers,
469        serials,
470        identifiers,
471        unreadable,
472    })
473}
474
475fn sort_error(error: SortError<Error>, origin: Option<u64>) -> Error {
476    match error {
477        SortError::Resource(reason) => Error::admission(reason, origin),
478        SortError::Comparison(error) => error,
479    }
480}
481fn bytes_order<W: Work>(left: &[u8], right: &[u8], execution: &W, at: u64) -> Result<Ordering> {
482    execution
483        .charge_work(1 + left.len().max(right.len()) as u64)
484        .map_err(|reason| Error::resource(reason, "signerInfo.sid", at))?;
485    Ok(left.cmp(right))
486}
487fn lower_bound<W: Work>(
488    length: usize,
489    execution: &W,
490    origin: Option<u64>,
491    mut compare: impl FnMut(usize) -> Result<Ordering>,
492) -> Result<usize> {
493    let mut from = 0;
494    let mut to = length;
495    while from < to {
496        execution
497            .charge_work(1)
498            .map_err(|reason| Error::admission(reason, origin))?;
499        let middle = from + (to - from) / 2;
500        if compare(middle)? == Ordering::Less {
501            from = middle + 1;
502        } else {
503            to = middle;
504        }
505    }
506    Ok(from)
507}
508fn segmented_matches<W: Work>(
509    wanted: &crate::Octets<'_>,
510    held: &[u8],
511    execution: &W,
512    at: u64,
513) -> Result<bool> {
514    execution
515        .charge_work(1)
516        .map_err(|reason| Error::resource(reason, "signerInfo.sid", at))?;
517    if wanted.len() != held.len() {
518        return Ok(false);
519    }
520    let mut offset = 0;
521    for piece in wanted.pieces() {
522        let bytes = piece.as_bytes();
523        execution
524            .charge_work(1 + bytes.len() as u64)
525            .map_err(|reason| Error::resource(reason, "signerInfo.sid", at))?;
526        if bytes != &held[offset..offset + bytes.len()] {
527            return Ok(false);
528        }
529        offset += bytes.len();
530    }
531    debug_assert_eq!(offset, held.len());
532    Ok(true)
533}