Skip to main content

structured_xades/validation/
ref_status_roles.rs

1//! Ordinary revocation references versus actual exclusive credential CA targets.
2
3use super::{
4    CertificateRoleError, ReferenceKind, ReferenceSearch,
5    ref_catalogue::{CandidatePools, Memo},
6    ref_envelope::basic_payload,
7    ref_roles::role_error,
8    ref_status_required::{
9        self, CredentialRequired, CredentialStatusSource, PathStatusSource, Required,
10    },
11};
12use crate::{
13    Xades,
14    binary::{self, BinaryXml, Location},
15    certificate::{Agreement, DigestProvider, FormatDigest},
16    digest,
17    material::{
18        self, ExclusiveFindingKind,
19        exclusivity::{self, CaError, CaFact, ReferenceStatus},
20    },
21    prepare::{Preparation, Prepared},
22    timestamp::CarriedMaterial,
23    unsigned,
24    xml::{Access, Xml},
25};
26use core::ops::Range;
27use structured_asn1::memory::{MemoryError, OwnedMap, OwnedVec, Work};
28use structured_ltv::material::{ArtifactRef, ArtifactSource, ArtifactUses};
29use structured_ltv::timestamp::TimestampScope;
30use structured_pkix::{Certificate, ocsp::Response};
31
32/// Whole-envelope absence is an unproven relation, never a hash of a basic payload.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum StatusRoleAgreement {
35    /// Completed whole CRL/envelope comparison or explicit unavailable capability.
36    Compared(Agreement),
37    /// An actual basic response has no matching original whole envelope in the
38    /// supplied source pools. Its exclusion cannot be established by this query.
39    EnvelopeUnprovided,
40}
41
42/// Digest fact for an actual contributing required status source.
43pub struct StatusSourceComparison {
44    source: usize,
45    agreement: StatusRoleAgreement,
46}
47impl StatusSourceComparison {
48    /// Ordinal in the same report's original required-source catalogue.
49    #[must_use]
50    pub fn source(&self) -> usize {
51        self.source
52    }
53    /// Whole-value comparison or an explicit missing-envelope obligation.
54    #[must_use]
55    pub fn agreement(&self) -> StatusRoleAgreement {
56        self.agreement
57    }
58}
59
60/// One actual exclusive credential CA status target and a reference comparison.
61pub struct StatusRoleComparison<'certificate, 'document, N, D, G> {
62    artifact: ArtifactRef<'certificate, 'document>,
63    target: &'certificate Certificate<'document>,
64    kind: ExclusiveFindingKind,
65    agreement: StatusRoleAgreement,
66    envelope: Option<(ArtifactSource, Location<N, D, G>)>,
67}
68impl<'c, 'd, N: Copy, D: Copy, G: Copy> StatusRoleComparison<'c, 'd, N, D, G> {
69    /// Actual contributing complete/delta CRL or basic response, not its signer.
70    #[must_use]
71    pub fn artifact(&self) -> ArtifactRef<'c, 'd> {
72        self.artifact
73    }
74    /// Actual exclusive credential target CA whose status is referenced.
75    #[must_use]
76    pub fn target(&self) -> &'c Certificate<'d> {
77        self.target
78    }
79    /// Asserted forbidden CA versus indeterminate old-certificate CA evidence.
80    #[must_use]
81    pub fn kind(&self) -> ExclusiveFindingKind {
82        self.kind
83    }
84    /// Digest fact or the outstanding whole-envelope obligation.
85    #[must_use]
86    pub fn agreement(&self) -> StatusRoleAgreement {
87        self.agreement
88    }
89    /// First original whole envelope containing this exact known basic body.
90    /// None for a CRL or missing envelope; no serialization fabricates a source.
91    #[must_use]
92    pub fn envelope(&self) -> Option<(ArtifactSource, Location<N, D, G>)> {
93        self.envelope
94    }
95}
96
97/// One original CRLRef/OCSPRef/open-format entry and its candidate comparisons.
98pub struct StatusRoleRow<N> {
99    property: usize,
100    entry: usize,
101    node: N,
102    search: ReferenceSearch,
103    range: Range<usize>,
104    root_range: Range<usize>,
105    credential_range: Range<usize>,
106    role: unsigned::Role,
107}
108impl<N: Copy> StatusRoleRow<N> {
109    /// Original property ordinal in this preparation.
110    #[must_use]
111    pub fn property(&self) -> usize {
112        self.property
113    }
114    /// Original entry ordinal in that property.
115    #[must_use]
116    pub fn entry(&self) -> usize {
117        self.entry
118    }
119    /// Original reference element.
120    #[must_use]
121    pub fn node(&self) -> N {
122        self.node
123    }
124    /// Complete digest search or explicit identifier/format/method obligation.
125    #[must_use]
126    pub fn search(&self) -> ReferenceSearch {
127        self.search
128    }
129    /// Original ordinary versus attribute revocation-reference property role.
130    #[must_use]
131    pub fn role(&self) -> unsigned::Role {
132        self.role
133    }
134}
135
136/// Required signer-path sources and A.1.2(5) comparisons from the complete
137/// actual actor/status closure. This is not full identifier/reference validity,
138/// status usability, anchor-reference exclusion or a grade.
139pub struct RevocationRoles<'prepared, 'certificate, 'document, N, D, G> {
140    prepared: &'prepared Preparation<N, D, G>,
141    rows: OwnedVec<StatusRoleRow<N>>,
142    comparisons: OwnedVec<StatusRoleComparison<'certificate, 'document, N, D, G>>,
143    root_sources: OwnedVec<PathStatusSource<'certificate, 'document>>,
144    root_comparisons: OwnedVec<StatusSourceComparison>,
145    root_present: bool,
146    credential_sources: OwnedVec<CredentialStatusSource<'certificate, 'document>>,
147    credential_comparisons: OwnedVec<StatusSourceComparison>,
148    credential_present: bool,
149}
150impl<'p, 'c, 'd, N: Copy, D, G> RevocationRoles<'p, 'c, 'd, N, D, G> {
151    /// Same complete original preparation.
152    #[must_use]
153    pub fn prepared(&self) -> &'p Preparation<N, D, G> {
154        self.prepared
155    }
156    /// All ordinary/attribute revocation references, retaining unsupported rows.
157    #[must_use]
158    pub fn rows(&self) -> &[StatusRoleRow<N>] {
159        &self.rows
160    }
161    /// Every relevant actual target/status comparison for this original row.
162    #[must_use]
163    pub fn comparisons(&self, row: usize) -> Option<&[StatusRoleComparison<'c, 'd, N, D, G>]> {
164        self.comparisons.get(self.rows.get(row)?.range.clone())
165    }
166    /// Evaluated complete/delta/OCSP contributors of the actual root path.
167    /// Unavailable rows may prevent lazy collection; an empty catalogue alone
168    /// does not prove that the path has no required sources.
169    #[must_use]
170    pub fn root_sources(&self) -> &[PathStatusSource<'c, 'd>] {
171        &self.root_sources
172    }
173    /// Compatible root-source comparisons for the same original reference row.
174    #[must_use]
175    pub fn root_comparisons(&self, row: usize) -> Option<&[StatusSourceComparison]> {
176        self.root_comparisons
177            .get(self.rows.get(row)?.root_range.clone())
178    }
179    /// A.1.2(1–2)'s required-source presence for each written ordinary property.
180    /// No optional property means no activated obligation. This does not prove
181    /// identifier correctness, anchor-reference exclusion, status/trust or grade.
182    #[must_use]
183    pub fn all_root_sources_referenced(&self) -> bool {
184        self.root_present
185    }
186    /// Evaluated credential path status contributors; anchors have no lookup.
187    /// Optional absence/unavailable rows may leave lazy collection unperformed;
188    /// an empty catalogue alone is not proof that no contributors are required.
189    #[must_use]
190    pub fn credential_sources(&self) -> &[CredentialStatusSource<'c, 'd>] {
191        &self.credential_sources
192    }
193    /// Every compatible credential-source comparison for the original row.
194    #[must_use]
195    pub fn credential_comparisons(&self, row: usize) -> Option<&[StatusSourceComparison]> {
196        self.credential_comparisons
197            .get(self.rows.get(row)?.credential_range.clone())
198    }
199    /// A.1.4(1–2)'s required-source presence, including ordinary alternatives.
200    /// Optional absence activates no obligation. This does not prove identifier
201    /// correctness, trust-anchor exclusions, status usability or baseline grade.
202    #[must_use]
203    pub fn all_credential_sources_referenced(&self) -> bool {
204        self.credential_present
205    }
206}
207
208/// Terminal XML/provider/admission and original target CA classification causes.
209pub type RevocationRoleResult<'p, 'c, 'd, X, PE> = binary::BinaryResult<
210    RevocationRoles<'p, 'c, 'd, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>,
211    X,
212    CertificateRoleError<'c, 'd, PE>,
213>;
214
215#[derive(Clone, Copy)]
216struct Envelope<'source, N, D, G> {
217    bytes: &'source [u8],
218    origin: ArtifactSource,
219    location: Location<N, D, G>,
220}
221
222type EnvelopeIndex<'s, N, D, G, W> = OwnedMap<&'s [u8], Envelope<'s, N, D, G>, W>;
223type XmlEnvelope<'s, X> =
224    Envelope<'s, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>;
225type XmlEnvelopeIndex<'s, X, W> =
226    EnvelopeIndex<'s, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation, W>;
227type ArtifactAnswer<'s, X> = (StatusRoleAgreement, Option<XmlEnvelope<'s, X>>);
228type OriginalRow<'a, X> = (
229    usize,
230    &'a super::ReferenceProperty<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>,
231    usize,
232);
233type StatusResult<'c, 'd, T, E> = Result<T, digest::Error<CertificateRoleError<'c, 'd, E>, E>>;
234type LocatedEnvelopes<'s, X, W> = OwnedMap<usize, Option<XmlEnvelope<'s, X>>, W>;
235type RoleComparisons<'c, 'd, X, W> = OwnedVec<
236    StatusRoleComparison<'c, 'd, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>,
237    W,
238>;
239
240fn insert_envelope<'source, N: Copy, D: Copy, G: Copy, W: Work>(
241    index: &mut OwnedMap<&'source [u8], Envelope<'source, N, D, G>, W>,
242    response: &structured_pkix::ocsp::ResponseBytes<'source>,
243    origin: ArtifactSource,
244    location: Location<N, D, G>,
245    work: &W,
246) -> Result<(), MemoryError> {
247    let Some(payload) = basic_payload(response, work)? else {
248        return Ok(());
249    };
250    if index.get(&payload, work)?.is_none() {
251        index.try_insert(
252            payload,
253            Envelope {
254                bytes: response.as_bytes(),
255                origin,
256                location,
257            },
258        )?;
259    }
260    Ok(())
261}
262
263fn envelope_index<'source, N: Copy, D: Copy, G: Copy, W: Work>(
264    pools: CandidatePools<'_, 'source, N, D, G>,
265    work: &W,
266) -> Result<EnvelopeIndex<'source, N, D, G, W>, MemoryError> {
267    let mut index = work.map();
268    for (ordinal, value) in pools.xml.values().iter().enumerate() {
269        work.charge_work(1)?;
270        if let material::Value::Ocsp(Response::Successful(response)) = &value.value {
271            insert_envelope(
272                &mut index,
273                response,
274                ArtifactSource::Material(ordinal),
275                value.location,
276                work,
277            )?;
278        }
279    }
280    if let Some(tokens) = pools.tokens {
281        for response in tokens.responses() {
282            work.charge_work(1)?;
283            let source = &tokens.view.sources()[response.origin.token];
284            insert_envelope(
285                &mut index,
286                &response.value,
287                ArtifactSource::TokenRevocation(response.origin),
288                source.location(),
289                work,
290            )?;
291        }
292    }
293    Ok(index)
294}
295
296struct StatusComparisons<
297    'provider,
298    'source,
299    'certificate,
300    'document,
301    X: Xml,
302    P: DigestProvider<'provider, X>,
303    W: Work,
304> {
305    memo: Memo<'provider, 'source, P, W>,
306    candidates: Option<OwnedVec<ReferenceStatus<'certificate, 'document>, W>>,
307    envelopes: Option<XmlEnvelopeIndex<'source, X, W>>,
308    located: LocatedEnvelopes<'source, X, W>,
309    comparisons: RoleComparisons<'certificate, 'document, X, W>,
310    required: Option<Required<'certificate, 'document, W>>,
311    root_comparisons: OwnedVec<StatusSourceComparison, W>,
312    credential_required: Option<CredentialRequired<'certificate, 'document, W>>,
313    credential_comparisons: OwnedVec<StatusSourceComparison, W>,
314    attribute_requested: bool,
315    covered: OwnedVec<bool, W>,
316    crl_ids: OwnedMap<&'document [u8], usize, W>,
317    ocsp_ids: OwnedMap<&'document [u8], usize, W>,
318    next_id: usize,
319}
320impl<'provider, 'source, 'c, 'd, X: Xml, P: DigestProvider<'provider, X>, W: Work>
321    StatusComparisons<'provider, 'source, 'c, 'd, X, P, W>
322{
323    fn new(provider: &'provider P, work: &W) -> Self {
324        Self {
325            memo: Memo::new(provider, work),
326            candidates: None,
327            envelopes: None,
328            located: work.map(),
329            comparisons: work.vector(),
330            required: None,
331            root_comparisons: work.vector(),
332            credential_required: None,
333            credential_comparisons: work.vector(),
334            attribute_requested: false,
335            covered: work.vector(),
336            crl_ids: work.map(),
337            ocsp_ids: work.map(),
338            next_id: 0,
339        }
340    }
341    fn envelope(
342        &mut self,
343        ordinal: usize,
344        body: &[u8],
345        pools: CandidatePools<'_, 'source, X::Id, X::Document, X::Generation>,
346        work: &W,
347    ) -> Result<Option<XmlEnvelope<'source, X>>, MemoryError> {
348        if let Some(&found) = self.located.get(&ordinal, work)? {
349            return Ok(found);
350        }
351        if self.envelopes.is_none() {
352            self.envelopes = Some(envelope_index(pools, work)?);
353        }
354        let found = self
355            .envelopes
356            .as_ref()
357            .expect("initialized envelope index")
358            .get(&body, work)?
359            .copied();
360        self.located.try_insert(ordinal, found)?;
361        Ok(found)
362    }
363    fn agreement(
364        &mut self,
365        artifact: ArtifactRef<'c, 'd>,
366        method: usize,
367        expected: &[u8],
368        pools: CandidatePools<'_, 'source, X::Id, X::Document, X::Generation>,
369        work: &W,
370    ) -> StatusResult<'c, 'd, ArtifactAnswer<'source, X>, P::Error> {
371        let (bytes, ids) = match artifact {
372            ArtifactRef::Crl(crl) => (crl.as_bytes(), &mut self.crl_ids),
373            ArtifactRef::Ocsp(basic) => (basic.as_bytes(), &mut self.ocsp_ids),
374            ArtifactRef::Certificate(_) => unreachable!("private status candidate"),
375        };
376        let ordinal =
377            if let Some(&ordinal) = ids.get(&bytes, work).map_err(digest::Error::Resource)? {
378                ordinal
379            } else {
380                let ordinal = self.next_id;
381                self.next_id = ordinal
382                    .checked_add(1)
383                    .ok_or(digest::Error::Resource(MemoryError::WorkExceeded))?;
384                ids.try_insert(bytes, ordinal)
385                    .map_err(digest::Error::Resource)?;
386                ordinal
387            };
388        let envelope = match artifact {
389            ArtifactRef::Ocsp(_) => self
390                .envelope(ordinal, bytes, pools, work)
391                .map_err(digest::Error::Resource)?,
392            _ => None,
393        };
394        let bytes = match artifact {
395            ArtifactRef::Crl(_) => Some(bytes),
396            _ => envelope.map(|value| value.bytes),
397        };
398        let agreement = if let Some(bytes) = bytes {
399            StatusRoleAgreement::Compared(
400                self.memo
401                    .compare(ordinal, bytes, method, expected, work)
402                    .map_err(|cause| digest::Error::Source(CertificateRoleError::Digest(cause)))?,
403            )
404        } else {
405            StatusRoleAgreement::EnvelopeUnprovided
406        };
407        Ok((agreement, envelope))
408    }
409    fn compare_root(
410        &mut self,
411        kind: ReferenceKind,
412        method: usize,
413        expected: &[u8],
414        pools: CandidatePools<'_, 'source, X::Id, X::Document, X::Generation>,
415        work: &W,
416    ) -> Result<(), digest::Error<CertificateRoleError<'c, 'd, P::Error>, P::Error>> {
417        let count = self
418            .required
419            .as_ref()
420            .expect("prepared root requirements")
421            .sources
422            .len();
423        for ordinal in 0..count {
424            work.charge_work(1).map_err(digest::Error::Resource)?;
425            let artifact = self.required.as_ref().expect("root sources").sources[ordinal].artifact;
426            if !compatible(kind, artifact) {
427                continue;
428            }
429            self.root_comparisons
430                .try_reserve(1)
431                .map_err(digest::Error::Resource)?;
432            let (agreement, _) = self.agreement(artifact, method, expected, pools, work)?;
433            if agreement == StatusRoleAgreement::Compared(Agreement::Matches) {
434                self.covered[ordinal] = true;
435            }
436            self.root_comparisons
437                .try_push(StatusSourceComparison {
438                    source: ordinal,
439                    agreement,
440                })
441                .map_err(digest::Error::Resource)?;
442        }
443        Ok(())
444    }
445    fn compare(
446        &mut self,
447        selection: (ReferenceKind, unsigned::Role),
448        identity: FormatDigest<P::Identity>,
449        expected: &[u8],
450        artifacts: &ArtifactUses<'_, '_, '_, 'c, 'd, X::Id, X::Document, X::Generation, Xades>,
451        pools: CandidatePools<'_, 'source, X::Id, X::Document, X::Generation>,
452        work: &W,
453    ) -> Result<(), digest::Error<CertificateRoleError<'c, 'd, P::Error>, P::Error>> {
454        let (kind, role) = selection;
455        let method = self
456            .memo
457            .method(identity, work)
458            .map_err(|cause| digest::Error::Source(CertificateRoleError::Digest(cause)))?;
459        self.compare_credentials((kind, role), method, expected, artifacts, pools, work)?;
460        if role != unsigned::Role::RevocationReferences {
461            return Ok(());
462        }
463        if self.required.is_none() {
464            let required =
465                ref_status_required::collect(artifacts, work).map_err(digest::Error::Resource)?;
466            self.covered
467                .try_reserve_exact(required.sources.len())
468                .map_err(digest::Error::Resource)?;
469            for _ in &required.sources {
470                self.covered
471                    .try_push(false)
472                    .map_err(digest::Error::Resource)?;
473            }
474            self.required = Some(required);
475        }
476        self.compare_root(kind, method, expected, pools, work)?;
477        if self.candidates.is_none() {
478            self.candidates = Some(
479                exclusivity::reference_status_candidates(artifacts, work).map_err(|cause| {
480                    digest::Error::Source(match cause {
481                        CaError::Resource(cause) => CertificateRoleError::Resource(cause),
482                        CaError::Certificate(certificate, cause) => {
483                            CertificateRoleError::Certificate { certificate, cause }
484                        }
485                    })
486                })?,
487            );
488        }
489        let count = self
490            .candidates
491            .as_ref()
492            .expect("initialized actual status candidates")
493            .len();
494        for ordinal in 0..count {
495            work.charge_work(1).map_err(digest::Error::Resource)?;
496            let candidate = &self.candidates.as_ref().expect("initialized candidates")[ordinal];
497            let artifact = candidate.artifact;
498            let target = candidate.certificate;
499            let ca = candidate.fact;
500            if !compatible(kind, artifact) {
501                continue;
502            }
503            self.comparisons
504                .try_reserve(1)
505                .map_err(digest::Error::Resource)?;
506            let (agreement, envelope) = self.agreement(artifact, method, expected, pools, work)?;
507            let kind = match ca {
508                CaFact::Asserted => ExclusiveFindingKind::ForbiddenCa,
509                CaFact::Unstated => ExclusiveFindingKind::UnstatedCa,
510                CaFact::NotAsserted => unreachable!("non-CA is not an exclusion candidate"),
511            };
512            self.comparisons
513                .try_push(StatusRoleComparison {
514                    artifact,
515                    target,
516                    kind,
517                    agreement,
518                    envelope: envelope.map(|value| (value.origin, value.location)),
519                })
520                .map_err(digest::Error::Resource)?;
521        }
522        Ok(())
523    }
524    fn compare_credentials(
525        &mut self,
526        (kind, role): (ReferenceKind, unsigned::Role),
527        method: usize,
528        expected: &[u8],
529        artifacts: &ArtifactUses<'_, '_, '_, 'c, 'd, X::Id, X::Document, X::Generation, Xades>,
530        pools: CandidatePools<'_, 'source, X::Id, X::Document, X::Generation>,
531        work: &W,
532    ) -> StatusResult<'c, 'd, (), P::Error> {
533        if !self.attribute_requested {
534            return Ok(());
535        }
536        if self.credential_required.is_none() {
537            self.credential_required = Some(
538                ref_status_required::collect_credentials(artifacts, work)
539                    .map_err(digest::Error::Resource)?,
540            );
541        }
542        let count = self
543            .credential_required
544            .as_ref()
545            .expect("collected credential statuses")
546            .sources
547            .len();
548        for ordinal in 0..count {
549            work.charge_work(1).map_err(digest::Error::Resource)?;
550            let artifact = self
551                .credential_required
552                .as_ref()
553                .expect("credential statuses")
554                .sources[ordinal]
555                .source
556                .artifact;
557            if !compatible(kind, artifact) {
558                continue;
559            }
560            self.credential_comparisons
561                .try_reserve(1)
562                .map_err(digest::Error::Resource)?;
563            let (agreement, _) = self.agreement(artifact, method, expected, pools, work)?;
564            if agreement == StatusRoleAgreement::Compared(Agreement::Matches) {
565                let source = &mut self
566                    .credential_required
567                    .as_mut()
568                    .expect("credential statuses")
569                    .sources[ordinal];
570                match role {
571                    unsigned::Role::RevocationReferences => source.ordinary = true,
572                    unsigned::Role::AttributeRevocationReferences => source.attribute = true,
573                    _ => unreachable!("private revocation property roles"),
574                }
575            }
576            self.credential_comparisons
577                .try_push(StatusSourceComparison {
578                    source: ordinal,
579                    agreement,
580                })
581                .map_err(digest::Error::Resource)?;
582        }
583        Ok(())
584    }
585    fn row(
586        &mut self,
587        xml: &X,
588        (property, refs, entry): OriginalRow<'_, X>,
589        artifacts: &ArtifactUses<'_, '_, '_, 'c, 'd, X::Id, X::Document, X::Generation, Xades>,
590        pools: CandidatePools<'_, 'source, X::Id, X::Document, X::Generation>,
591        provider: &'provider P,
592        work: &W,
593    ) -> binary::BinaryResult<StatusRoleRow<X::Id>, X, CertificateRoleError<'c, 'd, P::Error>>
594    where
595        X: BinaryXml,
596    {
597        let reference = &refs.entries()[entry];
598        let start = self.comparisons.len();
599        let root_start = self.root_comparisons.len();
600        let credential_start = self.credential_comparisons.len();
601        let search = if reference.kind() == ReferenceKind::Other {
602            ReferenceSearch::OtherFormat
603        } else if let Some((method, value)) = reference.digest() {
604            match digest::check_with(
605                xml,
606                digest::Reference {
607                    snapshot: refs.snapshot(),
608                    method,
609                    value,
610                },
611                provider,
612                work,
613                |identity, expected, work| {
614                    self.compare(
615                        (reference.kind(), refs.role()),
616                        identity,
617                        expected,
618                        artifacts,
619                        pools,
620                        work,
621                    )
622                },
623            )
624            .map_err(|cause| cause.map_callback(role_error))?
625            {
626                digest::Checked::Value(()) => ReferenceSearch::Compared,
627                digest::Checked::Answer(answer) => ReferenceSearch::Unavailable(answer),
628            }
629        } else {
630            ReferenceSearch::IdentifierOnly
631        };
632        Ok(StatusRoleRow {
633            property,
634            entry,
635            node: reference.node(),
636            search,
637            role: refs.role(),
638            range: start..self.comparisons.len(),
639            root_range: root_start..self.root_comparisons.len(),
640            credential_range: credential_start..self.credential_comparisons.len(),
641        })
642    }
643}
644
645fn compatible(kind: ReferenceKind, artifact: ArtifactRef<'_, '_>) -> bool {
646    matches!(
647        (kind, artifact),
648        (ReferenceKind::Crl, ArtifactRef::Crl(_)) | (ReferenceKind::Ocsp, ArtifactRef::Ocsp(_))
649    )
650}
651
652/// Check Annex A.1.2(5) using actual revocation TARGET CA facts and all actor
653/// purposes, not the authentication signer's CA bit. External complete/delta
654/// CRLs can be compared directly; OCSP must locate an original WHOLE envelope
655/// containing the exact basic response in the XML/token pools. Missing envelopes
656/// stay explicitly unproven, never replaced with a payload hash/re-encoding.
657/// Method/width checks precede lazy classification and source indexing; one
658/// memo and one body/source index serve every original reference. Neither XML,
659/// ASN.1, signature, path nor status authentication repeats. A late refusal drops
660/// the report. Unsupported methods and identifier-only/open references retain
661/// their obligations; this report cannot assert all reference requirements.
662/// Also retains every actual signer-path complete/delta/OCSP contributor and
663/// checks its required presence independently for each written property under
664/// A.1.2(1–2). All sources share the same exact-kind/byte identity memo, including
665/// artifacts reused by different path members. Optional absent properties do
666/// not allocate or activate this requirement. Hash identity alone does not
667/// validate mandatory issuer/responder/time fields or trust-anchor exclusions.
668/// A.1.4(1–2)'s credential sources are derived through the same path/status
669/// walker when the attribute-reference property is written. Ordinary matches
670/// satisfy its alternatives without erasing A.1.2(5)'s separate prohibitions;
671/// repeated attribute copies expose the SHOULD NOT advisory. Complete/delta
672/// and whole OCSP comparisons share the same memo across both property roles.
673/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
674///
675/// # Panics
676/// Only for a violated privately constructed immutable candidate/source ordinal.
677pub fn check_revocation_reference_roles<
678    'p,
679    'c,
680    'd,
681    'q,
682    X: BinaryXml,
683    P: DigestProvider<'q, X>,
684    W: Work,
685>(
686    xml: &X,
687    prepared: &'p Prepared<X>,
688    artifacts: &ArtifactUses<'_, '_, '_, 'c, 'd, X::Id, X::Document, X::Generation, Xades>,
689    view: &material::View<'_, X::Id, X::Document, X::Generation>,
690    tokens: Option<&CarriedMaterial<'_, X::Id, X::Document, X::Generation>>,
691    provider: &'q P,
692    work: &W,
693) -> RevocationRoleResult<'p, 'c, 'd, X, P::Error> {
694    let source = prepared.bound().containers();
695    let location = Location {
696        snapshot: source.snapshot(),
697        node: source.signature(),
698    };
699    let access = Access::new(xml, location.snapshot, work);
700    access
701        .query(location.node, |_, _, _| Ok(()))
702        .map_err(|cause| binary::Error::Access { location, cause })?;
703    let failed = |cause| binary::Error::Callback { location, cause };
704    let resource = |cause| failed(CertificateRoleError::Resource(cause));
705    let required = artifacts.inputs().requirements();
706    let pools = CandidatePools { xml: view, tokens };
707    if required.scope().location().snapshot != location.snapshot
708        || !matches!(required.scope(), TimestampScope::All(_))
709        || !access
710            .equal_id(required.signature(), location.node)
711            .map_err(resource)?
712        || !pools.same_source(location, &access).map_err(resource)?
713    {
714        return Err(failed(CertificateRoleError::Scope));
715    }
716    let mut state = StatusComparisons::<X, P, W>::new(provider, work);
717    for property in prepared.validation_references() {
718        work.charge_work(1).map_err(resource)?;
719        state.attribute_requested |=
720            property.role() == unsigned::Role::AttributeRevocationReferences;
721    }
722    let mut rows = work.vector();
723    let mut root_present = true;
724    for (property, refs) in prepared.validation_references().iter().enumerate() {
725        work.charge_work(1).map_err(resource)?;
726        if !matches!(
727            refs.role(),
728            unsigned::Role::RevocationReferences | unsigned::Role::AttributeRevocationReferences
729        ) {
730            continue;
731        }
732        if refs.role() == unsigned::Role::RevocationReferences {
733            for covered in &mut state.covered {
734                work.charge_work(1).map_err(resource)?;
735                *covered = false;
736            }
737        }
738        for entry in 0..refs.entries().len() {
739            work.charge_work(1).map_err(resource)?;
740            let row = state.row(
741                xml,
742                (property, refs, entry),
743                artifacts,
744                pools,
745                provider,
746                work,
747            )?;
748            rows.try_push(row).map_err(resource)?;
749        }
750        if refs.role() == unsigned::Role::AttributeRevocationReferences {
751            continue;
752        }
753        if let Some(required) = &state.required {
754            root_present &= !required.missing_input;
755            for &covered in &state.covered {
756                work.charge_work(1).map_err(resource)?;
757                root_present &= covered;
758            }
759        } else {
760            root_present = false;
761        }
762    }
763    let mut credential_present = !state.attribute_requested;
764    if let Some(required) = &state.credential_required {
765        credential_present = !required.missing_input;
766        for source in &required.sources {
767            work.charge_work(1).map_err(resource)?;
768            credential_present &= source.ordinary || source.attribute;
769        }
770    }
771    access
772        .query(location.node, |_, _, _| Ok(()))
773        .map_err(|cause| binary::Error::Access { location, cause })?;
774    Ok(RevocationRoles {
775        prepared,
776        rows: rows.into_owned(),
777        comparisons: state.comparisons.into_owned(),
778        root_sources: state.required.map_or_else(
779            || work.vector().into_owned(),
780            |required| required.sources.into_owned(),
781        ),
782        root_comparisons: state.root_comparisons.into_owned(),
783        root_present,
784        credential_sources: state.credential_required.map_or_else(
785            || work.vector().into_owned(),
786            |required| required.sources.into_owned(),
787        ),
788        credential_comparisons: state.credential_comparisons.into_owned(),
789        credential_present,
790    })
791}