Skip to main content

structured_ltv/pki/
crl.rs

1//! CRL authority paths bound to an actual certificate path and original source.
2
3use super::{CheckedPath, exact};
4use structured_asn1::memory::{MemoryError, Work};
5use structured_pkix::{
6    crl::{AbsenceEvidence, Authenticated, EvidenceSource},
7    path::WorkingKey,
8    temporal::{LeapTable, Reading, Uncertainty},
9};
10
11/// Authority path already checked by the common PKIX processor.
12#[derive(Clone, Copy)]
13pub enum CrlAuthority<'view, 'document> {
14    /// The target path's explicit trust anchor signs directly. This does not
15    /// turn the anchor certificate into a prospective path member.
16    Anchor,
17    /// Original checked path whose target is the actual CRL signing certificate.
18    Path(&'view CheckedPath<'document>),
19}
20
21/// Each original list needs its actual signing authority, including both lists
22/// in a complete/delta pair. A delta never substitutes for its base's path.
23#[derive(Clone, Copy)]
24pub enum CrlAuthorities<'view, 'document> {
25    /// One original complete CRL.
26    Complete(CrlAuthority<'view, 'document>),
27    /// Original complete and updating delta CRLs.
28    Delta {
29        /// Authority of the complete list.
30        base: CrlAuthority<'view, 'document>,
31        /// Authority of the updating list.
32        delta: CrlAuthority<'view, 'document>,
33    },
34}
35
36/// Every contributing reason source bound to one actual checked path member.
37/// This retains the original union and authority inputs without copying their
38/// catalogues. Partial reason coverage remains partial; signer revocation and
39/// application trust are separate obligations.
40pub struct CrlCoverage<'view, 'certificate, 'document> {
41    path: &'view CheckedPath<'document>,
42    member: usize,
43    coverage: &'view structured_pkix::crl::Coverage<'certificate, 'document>,
44    authorities: &'view [CrlAuthorities<'view, 'document>],
45}
46
47impl<'view, 'certificate, 'document> CrlCoverage<'view, 'certificate, 'document> {
48    /// Same original target path.
49    #[must_use]
50    pub fn path(&self) -> &'view CheckedPath<'document> {
51        self.path
52    }
53    /// Original non-anchor member ordinal.
54    #[must_use]
55    pub fn member(&self) -> usize {
56        self.member
57    }
58    /// All original contributing complete/delta sources and their reason union.
59    #[must_use]
60    pub fn coverage(&self) -> &'view structured_pkix::crl::Coverage<'certificate, 'document> {
61        self.coverage
62    }
63    /// Actual authority inputs, in the original contributing-source order.
64    #[must_use]
65    pub fn authorities(&self) -> &'view [CrlAuthorities<'view, 'document>] {
66        self.authorities
67    }
68}
69
70/// Complete association, or the exact contributing source that mismatched.
71pub enum CrlCoverageBinding<'view, 'certificate, 'document> {
72    /// Every contributor is associated; reason completeness is still explicit.
73    Bound(CrlCoverage<'view, 'certificate, 'document>),
74    /// No successful partial receipt is returned for earlier contributors.
75    Rejected {
76        /// Original source ordinal in the coverage union.
77        source: usize,
78        /// Actual completed mismatch.
79        cause: CrlBindingFailure,
80    },
81}
82
83/// Inability to associate all contributors, never an absent/Good status.
84#[derive(Debug)]
85pub enum CrlCoverageError {
86    /// One authority input is required for every original reason contributor.
87    AuthorityCount,
88    /// Original source ordinal and processing/resource cause.
89    Source {
90        /// Original ordinal in the reason-union catalogue.
91        source: usize,
92        /// Original processing refusal; not a semantic absence.
93        cause: CrlBindingError,
94    },
95}
96
97/// Completed mismatch, separately from a resource or processing refusal.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum CrlBindingFailure {
100    /// The absence was established for another original certificate.
101    Target,
102    /// The CRL signer path ends at a different trusted name/key.
103    Anchor,
104    /// The checked authority is not the actual CRL signer.
105    Signer,
106    /// Actual signature verification used different inherited parameters.
107    EffectiveKey,
108    /// Authority path does not cover the full status-evaluation reading.
109    Interval,
110}
111
112/// Inability to complete association; these causes are never a status result.
113#[derive(Debug)]
114pub enum CrlBindingError {
115    /// There is no such non-anchor target member.
116    MemberIndex {
117        /// Caller-supplied issuer-to-subject ordinal.
118        index: usize,
119    },
120    /// Complete/delta authority inputs do not match the original evidence kind.
121    SourceKind,
122    /// Current execution refused before traversal, comparison or UTC lookup.
123    Resource(MemoryError),
124    /// Original shared Name comparison cause.
125    Name(structured_pkix::Error),
126    /// Shared UTC facts cannot establish the evaluation label.
127    Time(Uncertainty),
128}
129
130impl From<super::authority::Error> for CrlBindingError {
131    fn from(cause: super::authority::Error) -> Self {
132        match cause {
133            super::authority::Error::Resource(cause) => Self::Resource(cause),
134            super::authority::Error::Name(cause) => Self::Name(cause),
135            super::authority::Error::Time(cause) => Self::Time(cause),
136        }
137    }
138}
139
140impl From<super::authority::Failure> for CrlBindingFailure {
141    fn from(cause: super::authority::Failure) -> Self {
142        match cause {
143            super::authority::Failure::Anchor => Self::Anchor,
144            super::authority::Failure::Signer => Self::Signer,
145            super::authority::Failure::EffectiveKey => Self::EffectiveKey,
146            super::authority::Failure::Interval => Self::Interval,
147        }
148    }
149}
150
151/// Target and signing-authority association, still not complete revocation
152/// coverage, CRL signer non-revocation or application trust/policy acceptance.
153pub struct CrlTarget<'view, 'certificate, 'document> {
154    path: &'view CheckedPath<'document>,
155    member: usize,
156    evidence: &'view AbsenceEvidence<'certificate, 'document>,
157    authorities: CrlAuthorities<'view, 'document>,
158}
159
160impl<'view, 'certificate, 'document> CrlTarget<'view, 'certificate, 'document> {
161    /// Actual target path, not another certificate sharing its serial.
162    #[must_use]
163    pub fn path(&self) -> &'view CheckedPath<'document> {
164        self.path
165    }
166    /// Original target's issuer-to-subject ordinal.
167    #[must_use]
168    pub fn member(&self) -> usize {
169        self.member
170    }
171    /// Same original qualified absence and source pair; no verification repeats.
172    #[must_use]
173    pub fn evidence(&self) -> &'view AbsenceEvidence<'certificate, 'document> {
174        self.evidence
175    }
176    /// Actual checked authority for each retained original list.
177    #[must_use]
178    pub fn authorities(&self) -> &CrlAuthorities<'view, 'document> {
179        &self.authorities
180    }
181}
182
183/// Complete association or a completed original-source mismatch.
184pub enum CrlBinding<'view, 'certificate, 'document> {
185    /// Actual target and each original signing authority are associated.
186    Bound(CrlTarget<'view, 'certificate, 'document>),
187    /// A required original target/path/key/time association does not hold.
188    Rejected(CrlBindingFailure),
189}
190
191#[derive(Clone, Copy)]
192pub(super) enum CrlSource<'view, 'certificate, 'document> {
193    Absence(&'view AbsenceEvidence<'certificate, 'document>),
194    Reading(super::CrlReading<'view, 'certificate, 'document>),
195}
196
197impl<'certificate, 'document> CrlSource<'_, 'certificate, 'document> {
198    fn target(self) -> &'certificate structured_pkix::Certificate<'document> {
199        match self {
200            Self::Absence(value) => value.target(),
201            Self::Reading(value) => value.target(),
202        }
203    }
204    fn at(self) -> Reading {
205        match self {
206            Self::Absence(value) => value.evaluated_at(),
207            Self::Reading(value) => value.evaluated_at(),
208        }
209    }
210    fn source(self) -> EvidenceSource<'certificate, 'document> {
211        match self {
212            Self::Absence(value) => value.source(),
213            Self::Reading(value) => value.source(),
214        }
215    }
216}
217
218impl<'document> CheckedPath<'document> {
219    pub(super) fn bind_crl_source<W: Work>(
220        &self,
221        member: usize,
222        facts: CrlSource<'_, '_, 'document>,
223        authorities: CrlAuthorities<'_, 'document>,
224        utc: &LeapTable<'_>,
225        execution: &W,
226    ) -> Result<Option<CrlBindingFailure>, CrlBindingError> {
227        execution
228            .charge_work(1)
229            .map_err(CrlBindingError::Resource)?;
230        let Some(expected) = self.members.get(member) else {
231            return Err(CrlBindingError::MemberIndex { index: member });
232        };
233        if !exact(
234            expected.certificate().as_bytes(),
235            facts.target().as_bytes(),
236            execution,
237        )
238        .map_err(CrlBindingError::Resource)?
239        {
240            return Ok(Some(CrlBindingFailure::Target));
241        }
242        // Normalize only after the current metadata admission above. Both
243        // variants originate in private common processing facts, not a caller
244        // supplied certificate/time tuple or a synthesized absence claim.
245        match (facts.source(), authorities) {
246            (EvidenceSource::Complete(source), CrlAuthorities::Complete(authority)) => {
247                self.crl_authority(source, authority, facts.at(), utc, execution)
248            }
249            (
250                EvidenceSource::Delta { base, delta },
251                CrlAuthorities::Delta {
252                    base: first,
253                    delta: second,
254                },
255            ) => {
256                if let Some(failure) =
257                    self.crl_authority(base, first, facts.at(), utc, execution)?
258                {
259                    return Ok(Some(failure));
260                }
261                self.crl_authority(delta, second, facts.at(), utc, execution)
262            }
263            _ => Err(CrlBindingError::SourceKind),
264        }
265    }
266
267    /// Bind every original reason contributor using the same target/key/time
268    /// kernel as a single source. RFC5280 §6.3.3's `reasons_mask` union cannot
269    /// inherit authenticated-path provenance from only its first contributor.
270    /// This traversal adds no catalogue allocation or signature verification;
271    /// shared Name comparison retains its ordinary scoped scratch admission.
272    /// <https://www.rfc-editor.org/rfc/rfc5280.html#section-6.3.3>
273    pub fn bind_crl_coverage<'view, 'certificate, W: Work>(
274        &'view self,
275        member: usize,
276        coverage: &'view structured_pkix::crl::Coverage<'certificate, 'document>,
277        authorities: &'view [CrlAuthorities<'view, 'document>],
278        utc: &LeapTable<'_>,
279        execution: &W,
280    ) -> Result<CrlCoverageBinding<'view, 'certificate, 'document>, CrlCoverageError> {
281        execution
282            .charge_work(1)
283            .map_err(|cause| CrlCoverageError::Source {
284                source: 0,
285                cause: CrlBindingError::Resource(cause),
286            })?;
287        if authorities.len() != coverage.sources().len() {
288            return Err(CrlCoverageError::AuthorityCount);
289        }
290        for (source, authority) in authorities.iter().enumerate() {
291            execution
292                .charge_work(1)
293                .map_err(|cause| CrlCoverageError::Source {
294                    source,
295                    cause: CrlBindingError::Resource(cause),
296                })?;
297            match self
298                .bind_crl(
299                    member,
300                    &coverage.sources()[source],
301                    *authority,
302                    utc,
303                    execution,
304                )
305                .map_err(|cause| CrlCoverageError::Source { source, cause })?
306            {
307                CrlBinding::Bound(_) => {}
308                CrlBinding::Rejected(cause) => {
309                    return Ok(CrlCoverageBinding::Rejected { source, cause });
310                }
311            }
312        }
313        Ok(CrlCoverageBinding::Bound(CrlCoverage {
314            path: self,
315            member,
316            coverage,
317            authorities,
318        }))
319    }
320
321    /// Bind one original qualified CRL source to this actual path member.
322    /// RFC5280 §6.3.3(f) requires the CRL issuer's path to use the same trust
323    /// anchor as the target path. Shared CRL authentication already checks the
324    /// actual signature and cRLSign; this operation binds its original signer
325    /// and effective key to the supplied checked authority, without repeating
326    /// math. An indirect CRL authority may differ from the target's issuer.
327    /// <https://www.rfc-editor.org/rfc/rfc5280.html#section-6.3.3>
328    ///
329    /// # Panics
330    /// Only if the private nonempty checked-path invariant is violated.
331    pub fn bind_crl<'view, 'certificate, W: Work>(
332        &'view self,
333        member: usize,
334        evidence: &'view AbsenceEvidence<'certificate, 'document>,
335        authorities: CrlAuthorities<'view, 'document>,
336        utc: &LeapTable<'_>,
337        execution: &W,
338    ) -> Result<CrlBinding<'view, 'certificate, 'document>, CrlBindingError> {
339        if let Some(failure) = self.bind_crl_source(
340            member,
341            CrlSource::Absence(evidence),
342            authorities,
343            utc,
344            execution,
345        )? {
346            return Ok(CrlBinding::Rejected(failure));
347        }
348        Ok(CrlBinding::Bound(CrlTarget {
349            path: self,
350            member,
351            evidence,
352            authorities,
353        }))
354    }
355
356    fn crl_authority<W: Work>(
357        &self,
358        source: Authenticated<'_, 'document>,
359        authority: CrlAuthority<'_, 'document>,
360        at: Reading,
361        utc: &LeapTable<'_>,
362        execution: &W,
363    ) -> Result<Option<CrlBindingFailure>, CrlBindingError> {
364        let resource = CrlBindingError::Resource;
365        execution.charge_work(1).map_err(resource)?;
366        let expected = match authority {
367            CrlAuthority::Anchor => {
368                if !self
369                    .anchor
370                    .name()
371                    .matches(source.issuer().tbs().subject(), execution)
372                    .map_err(CrlBindingError::Name)?
373                {
374                    return Ok(Some(CrlBindingFailure::Signer));
375                }
376                WorkingKey::standing(self.anchor.key())
377            }
378            CrlAuthority::Path(path) => {
379                return Ok(self
380                    .path_authority(path, source.issuer(), source.key(), at, utc, execution)?
381                    .map(Into::into));
382            }
383        };
384        if !exact(
385            expected.key().as_bytes(),
386            source.key().key().as_bytes(),
387            execution,
388        )
389        .map_err(resource)?
390        {
391            return Ok(Some(CrlBindingFailure::Signer));
392        }
393        let same = match (expected.parameters(), source.key().parameters()) {
394            (None, None) => true,
395            (Some(left), Some(right)) => exact(left, right, execution).map_err(resource)?,
396            _ => false,
397        };
398        if same {
399            return Ok(None);
400        }
401        Ok(Some(CrlBindingFailure::EffectiveKey))
402    }
403}