Skip to main content

structured_pkix/crl/
delta.rs

1//! Binding an authenticated delta to an authenticated complete CRL.
2
3use super::{Authenticated, EntryLookup, Reason, RevocationTarget, RevokedCertificate};
4use crate::error::{Error, ErrorCode, Located, Result};
5use crate::extension::{Extension, Extensions};
6use crate::identifier::AuthorityKeyIdentifier;
7use crate::oid;
8use core::cmp::Ordering;
9use structured_asn1::memory::Work;
10use structured_asn1::{Integer, Oid, Span};
11
12/// Required metadata that was absent, rather than a mismatched value.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum MissingDeltaField {
15    /// Complete CRL does not carry a CRL number.
16    BaseNumber,
17    /// Delta does not carry a CRL number.
18    DeltaNumber,
19    /// Candidate delta does not carry a deltaCRLIndicator.
20    Indicator,
21    /// Complete CRL omits the authority key identifier required by its profile.
22    BaseAuthorityIdentifier,
23    /// Delta omits the authority key identifier required by its profile.
24    DeltaAuthorityIdentifier,
25}
26
27/// Why two authenticated lists cannot form a complete-plus-delta pair.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum DeltaFailure {
30    /// The proposed base is itself a delta.
31    BaseIsDelta,
32    /// The CRLs name different issuing authorities.
33    Issuer,
34    /// Original public keys or their effective parameters differ.
35    SigningKey,
36    /// Issuing distribution point encodings differ, including presence.
37    Scope,
38    /// Base number precedes the delta's minimum base.
39    BaseTooOld,
40    /// Delta number is not strictly greater than the complete CRL number.
41    NumberOrder,
42    /// An extension's criticality contradicts its specified role.
43    Criticality,
44    /// Complete validated AKI payloads differ, including optional fields.
45    AuthorityIdentifier,
46}
47
48/// Authenticated source pair bound according to RFC 5280 §§5.2.4/6.3.3(c). Entries,
49/// freshness and critical demands still require interpretation before status.
50#[derive(Debug, Clone, Copy)]
51pub struct BoundDelta<'b, 'r, 'a> {
52    base: &'b Authenticated<'r, 'a>,
53    delta: &'b Authenticated<'r, 'a>,
54    delta_number: Integer<'a>,
55}
56
57/// Source of an entry in combined evidence; original ranges remain untouched.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum EntryOrigin {
60    /// The authenticated complete CRL.
61    Complete,
62    /// The authenticated delta CRL.
63    Delta,
64}
65
66/// Delta-first entry interpretation, without freshness or coverage claims.
67#[derive(Debug)]
68pub enum CombinedEntry<'r, 'a> {
69    /// Neither source lists this issuer/serial. This alone is not non-revocation.
70    Absent,
71    /// The selected original entry states revocation.
72    Revoked {
73        /// Source whose entry takes effect.
74        origin: EntryOrigin,
75        /// Original entry, including date and deferred metadata.
76        entry: &'r RevokedCertificate<'a>,
77        /// Encoded reason or the normative unspecified default.
78        reason: Reason,
79    },
80    /// Delta removes the entry. Full reason coverage is still required.
81    Removed {
82        /// Original delta removal entry.
83        entry: &'r RevokedCertificate<'a>,
84    },
85    /// One source contains ambiguous repeated identity.
86    Conflict {
87        /// Source containing both entries.
88        origin: EntryOrigin,
89        /// Earlier original entry.
90        first: &'r RevokedCertificate<'a>,
91        /// Later original entry.
92        second: &'r RevokedCertificate<'a>,
93    },
94    /// A critical demand anywhere prevents using this source pair.
95    Unsupported {
96        /// Source whose critical demand is not understood.
97        origin: EntryOrigin,
98        /// Demand identifier, preserved as borrowed source bytes.
99        kind: Oid<'a>,
100        /// Original demand location.
101        span: Span,
102    },
103}
104
105impl<'b, 'r, 'a> BoundDelta<'b, 'r, 'a> {
106    /// Compare the already validated delta CRL numbers without rereading them.
107    /// Numeric order establishes supersession only within the same issuer and
108    /// scope (RFC 5280 §5.2.3); applicability and selection remain with the caller.
109    /// <https://www.rfc-editor.org/rfc/rfc5280#section-5.2.3>
110    ///
111    /// Both delta producers and all current work ceilings fund the comparison,
112    /// with one aggregate bill and no allocation. A resource refusal is located
113    /// at this delta CRL's original start. Binding performs no extra comparison.
114    pub fn compare_delta_number<W: Work>(
115        self,
116        other: BoundDelta<'_, '_, '_>,
117        execution: &W,
118    ) -> Result<Ordering> {
119        const FIELD: &str = "certificateList.deltaNumberOrder";
120        let at = self.delta.crl().span().start;
121        let left = self.delta_number.as_bytes();
122        let right = other.delta_number.as_bytes();
123        // Pay for sign-octet inspection as well as magnitude comparison before
124        // examining either number. A current foreign source must not be replaced.
125        let work = comparison_work(left.len().max(right.len()), FIELD, at)?;
126        self.delta
127            .crl()
128            .signature_algorithm()
129            .execution_origin()
130            .charge_work_with(
131                other.delta.crl().signature_algorithm().execution_origin(),
132                execution,
133                work,
134            )
135            .map_err(|reason| Error::resource(reason, FIELD, at))?;
136        Ok(compare_magnitudes(magnitude(left), magnitude(right)))
137    }
138
139    /// Apply RFC5280 §6.3.3(i)–(k) over original authenticated sources.
140    /// Each source is scanned independently so indirect issuer state never
141    /// leaks between lists. Critical demands and conflicting duplicates are
142    /// checked in both sources before selecting a delta override.
143    /// <https://www.rfc-editor.org/rfc/rfc5280#section-6.3.3>
144    pub fn entry<'t, T: RevocationTarget<'t>, W: Work>(
145        self,
146        target: &T,
147        execution: &W,
148    ) -> Result<CombinedEntry<'r, 'a>> {
149        let base = match usable(self.base.entry(target, execution)?, EntryOrigin::Complete) {
150            Ok(entry) => entry,
151            Err(refusal) => return Ok(refusal),
152        };
153        let delta = match usable(self.delta.entry(target, execution)?, EntryOrigin::Delta) {
154            Ok(entry) => entry,
155            Err(refusal) => return Ok(refusal),
156        };
157        let (origin, entry) = match (base, delta) {
158            (_, Some(entry)) => (EntryOrigin::Delta, entry),
159            (Some(entry), None) => (EntryOrigin::Complete, entry),
160            (None, None) => return Ok(CombinedEntry::Absent),
161        };
162        let reason = entry.reason(execution)?.unwrap_or(Reason::Unspecified);
163        // RFC5280 §5.2.4 permits removal even when this base lacked a hold
164        // entry (intervening hold changes or expiration). Do not invent a
165        // certificateHold prerequisite or reuse the base entry after removal.
166        Ok(if reason == Reason::RemoveFromCrl {
167            CombinedEntry::Removed { entry }
168        } else {
169            CombinedEntry::Revoked {
170                origin,
171                entry,
172                reason,
173            }
174        })
175    }
176    /// Complete source CRL, not a serialized reconstructed list.
177    #[must_use]
178    pub fn base(self) -> &'b Authenticated<'r, 'a> {
179        self.base
180    }
181    /// Delta source CRL, whose update times govern the combined list.
182    #[must_use]
183    pub fn delta(self) -> &'b Authenticated<'r, 'a> {
184        self.delta
185    }
186}
187
188fn usable<'r, 'a>(
189    lookup: EntryLookup<'r, 'a>,
190    origin: EntryOrigin,
191) -> core::result::Result<Option<&'r RevokedCertificate<'a>>, CombinedEntry<'r, 'a>> {
192    match lookup {
193        EntryLookup::Conflict { first, second } => Err(CombinedEntry::Conflict {
194            origin,
195            first,
196            second,
197        }),
198        EntryLookup::Unsupported { kind, span } => {
199            Err(CombinedEntry::Unsupported { origin, kind, span })
200        }
201        EntryLookup::Absent => Ok(None),
202        EntryLookup::Found(entry) => Ok(Some(entry)),
203    }
204}
205
206/// Binding outcome, independent of processing/resource failures.
207#[derive(Debug)]
208pub enum DeltaBinding<'b, 'r, 'a> {
209    /// Issuer, effective signing key, scope and numbers are bound.
210    Bound(BoundDelta<'b, 'r, 'a>),
211    /// Required metadata is missing.
212    Missing(MissingDeltaField),
213    /// Metadata establishes that this pair cannot be combined.
214    Rejected(DeltaFailure),
215}
216
217impl<'r, 'a> Authenticated<'r, 'a> {
218    /// Bind a candidate delta to this complete CRL without copying entries.
219    /// The caller supplies both issuer paths' same-anchor trust provenance.
220    /// Each comparison and lazy read uses the current execution account.
221    pub fn bind_delta<'b, W: Work>(
222        &'b self,
223        delta: &'b Self,
224        execution: &W,
225    ) -> Result<DeltaBinding<'b, 'r, 'a>> {
226        const FIELD: &str = "certificateList.deltaBinding";
227        let at = delta.crl().span().start;
228        let base_extensions = self.crl().tbs().extensions();
229        let delta_extensions = delta.crl().tbs().extensions();
230        if base_extensions
231            .get(oid::DELTA_CRL_INDICATOR, execution)?
232            .is_some()
233        {
234            return Ok(DeltaBinding::Rejected(DeltaFailure::BaseIsDelta));
235        }
236        let Some(indicator) = delta_extensions.get(oid::DELTA_CRL_INDICATOR, execution)? else {
237            return Ok(DeltaBinding::Missing(MissingDeltaField::Indicator));
238        };
239        if !indicator.is_critical() {
240            return Ok(DeltaBinding::Rejected(DeltaFailure::Criticality));
241        }
242        let minimum = number(indicator, execution)?;
243        let Some(base_number) = base_extensions.get(oid::CRL_NUMBER, execution)? else {
244            return Ok(DeltaBinding::Missing(MissingDeltaField::BaseNumber));
245        };
246        let Some(delta_number) = delta_extensions.get(oid::CRL_NUMBER, execution)? else {
247            return Ok(DeltaBinding::Missing(MissingDeltaField::DeltaNumber));
248        };
249        if base_number.is_critical() || delta_number.is_critical() {
250            return Ok(DeltaBinding::Rejected(DeltaFailure::Criticality));
251        }
252        let base_number = number(base_number, execution)?;
253        let delta_number = number(delta_number, execution)?;
254        match authority_pair(base_extensions, delta_extensions, execution, at)? {
255            AuthorityMatch::Matched => (),
256            AuthorityMatch::Missing(field) => return Ok(DeltaBinding::Missing(field)),
257            AuthorityMatch::Rejected(reason) => return Ok(DeltaBinding::Rejected(reason)),
258        }
259        // RFC 5280 §5.2.4 conditions (a)-(d): same issuer, same signing key,
260        // same scope, baseNumber >= BaseCRLNumber and < delta's CRL number.
261        // https://www.rfc-editor.org/rfc/rfc5280#section-5.2.4
262        if !self
263            .crl()
264            .tbs()
265            .issuer()
266            .matches(delta.crl().tbs().issuer(), execution)?
267        {
268            return Ok(DeltaBinding::Rejected(DeltaFailure::Issuer));
269        }
270        if !equal(
271            self.key().key().as_bytes(),
272            delta.key().key().as_bytes(),
273            execution,
274            FIELD,
275            at,
276        )? || !optional_equal(
277            self.key().parameters(),
278            delta.key().parameters(),
279            execution,
280            FIELD,
281            at,
282        )? {
283            return Ok(DeltaBinding::Rejected(DeltaFailure::SigningKey));
284        }
285        let base_scope = base_extensions.get(oid::ISSUING_DISTRIBUTION_POINT, execution)?;
286        let delta_scope = delta_extensions.get(oid::ISSUING_DISTRIBUTION_POINT, execution)?;
287        if base_scope.is_some_and(|extension| !extension.is_critical())
288            || delta_scope.is_some_and(|extension| !extension.is_critical())
289        {
290            return Ok(DeltaBinding::Rejected(DeltaFailure::Criticality));
291        }
292        if !optional_equal(
293            base_scope.map(|extension| extension.value()),
294            delta_scope.map(|extension| extension.value()),
295            execution,
296            FIELD,
297            at,
298        )? {
299            return Ok(DeltaBinding::Rejected(DeltaFailure::Scope));
300        }
301        if compare(base_number, minimum, execution, FIELD, at)? == Ordering::Less {
302            return Ok(DeltaBinding::Rejected(DeltaFailure::BaseTooOld));
303        }
304        if compare(base_number, delta_number, execution, FIELD, at)? != Ordering::Less {
305            return Ok(DeltaBinding::Rejected(DeltaFailure::NumberOrder));
306        }
307        Ok(DeltaBinding::Bound(BoundDelta {
308            base: self,
309            delta,
310            delta_number,
311        }))
312    }
313}
314
315enum AuthorityMatch {
316    Matched,
317    Missing(MissingDeltaField),
318    Rejected(DeltaFailure),
319}
320
321fn authority_pair<'a, W: Work>(
322    base: &Extensions<'a>,
323    delta: &Extensions<'a>,
324    execution: &W,
325    at: u64,
326) -> Result<AuthorityMatch> {
327    let Some(base) = base.get(oid::AUTHORITY_KEY_IDENTIFIER, execution)? else {
328        return Ok(AuthorityMatch::Missing(
329            MissingDeltaField::BaseAuthorityIdentifier,
330        ));
331    };
332    let Some(delta) = delta.get(oid::AUTHORITY_KEY_IDENTIFIER, execution)? else {
333        return Ok(AuthorityMatch::Missing(
334            MissingDeltaField::DeltaAuthorityIdentifier,
335        ));
336    };
337    if base.is_critical() || delta.is_critical() {
338        return Ok(AuthorityMatch::Rejected(DeltaFailure::Criticality));
339    }
340    for extension in [base, delta] {
341        let parsed = AuthorityKeyIdentifier::read(extension, execution)?;
342        // RFC5280 §5.2.1 requires CRLs to carry the keyIdentifier method,
343        // not just the optional issuer/serial alternative.
344        if parsed.key_id().is_none() {
345            return Err(Error::new(
346                ErrorCode::MissingField,
347                "extensions.authorityKeyIdentifier.keyIdentifier",
348                extension.span().start,
349            ));
350        }
351    }
352    // RFC5280 §6.3.3(c)(3) requires matching extensions. It does not define
353    // additional DN/set equivalence here. Full validated DER-value equality
354    // follows OpenSSL check_delta_base and Bouncy Castle PKIXCRLValidator;
355    // optional field presence and order remain significant. Outer Extension
356    // headers are not compared; criticality is checked independently above.
357    // https://www.rfc-editor.org/rfc/rfc5280#section-6.3.3
358    // https://github.com/openssl/openssl/blob/2506f1fc323b567b0e48a13b6069c7778077e55e/crypto/x509/x509_vfy.c#L1482
359    Ok(
360        if equal(
361            base.value(),
362            delta.value(),
363            execution,
364            "certificateList.deltaBinding",
365            at,
366        )? {
367            AuthorityMatch::Matched
368        } else {
369            AuthorityMatch::Rejected(DeltaFailure::AuthorityIdentifier)
370        },
371    )
372}
373
374pub(super) fn number<'a, W: Work>(extension: &Extension<'a>, execution: &W) -> Result<Integer<'a>> {
375    const FIELD: &str = "extensions.cRLNumber";
376    let mut reader = extension.reader_scoped(execution).field(FIELD)?;
377    let value = reader.read_integer().field(FIELD)?;
378    super::end(&reader, FIELD, extension.span().start)?;
379    if value.is_negative() {
380        return Err(Error::new(
381            ErrorCode::MalformedValue,
382            FIELD,
383            extension.span().start,
384        ));
385    }
386    Ok(value)
387}
388
389fn compare(
390    left: Integer<'_>,
391    right: Integer<'_>,
392    execution: &impl Work,
393    field: &'static str,
394    at: u64,
395) -> Result<Ordering> {
396    let left = magnitude(left.as_bytes());
397    let right = magnitude(right.as_bytes());
398    // DER INTEGER is minimal; a possible zero sign octet has no magnitude.
399    admit(left.len().max(right.len()), execution, field, at)?;
400    Ok(compare_magnitudes(left, right))
401}
402
403fn compare_magnitudes(left: &[u8], right: &[u8]) -> Ordering {
404    left.len().cmp(&right.len()).then_with(|| left.cmp(right))
405}
406
407fn magnitude(bytes: &[u8]) -> &[u8] {
408    match bytes {
409        [0, rest @ ..] => rest,
410        all => all,
411    }
412}
413
414fn optional_equal(
415    left: Option<&[u8]>,
416    right: Option<&[u8]>,
417    execution: &impl Work,
418    field: &'static str,
419    at: u64,
420) -> Result<bool> {
421    execution
422        .charge_work(1)
423        .map_err(|reason| Error::resource(reason, field, at))?;
424    match (left, right) {
425        (Some(left), Some(right)) => equal(left, right, execution, field, at),
426        (None, None) => Ok(true),
427        _ => Ok(false),
428    }
429}
430
431fn equal(
432    left: &[u8],
433    right: &[u8],
434    execution: &impl Work,
435    field: &'static str,
436    at: u64,
437) -> Result<bool> {
438    admit(left.len().max(right.len()), execution, field, at)?;
439    Ok(left == right)
440}
441
442fn admit(width: usize, execution: &impl Work, field: &'static str, at: u64) -> Result<()> {
443    let work = comparison_work(width, field, at)?;
444    execution
445        .charge_work(work)
446        .map_err(|reason| Error::resource(reason, field, at))
447}
448
449fn comparison_work(width: usize, field: &'static str, at: u64) -> Result<u64> {
450    (width as u64)
451        .checked_mul(2)
452        .and_then(|n| n.checked_add(1))
453        .ok_or_else(|| {
454            Error::resource(
455                structured_asn1::memory::MemoryError::WorkExceeded,
456                field,
457                at,
458            )
459        })
460}