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