Skip to main content

structured_xades/validation/
ref_status_required.rs

1//! Actual path status inputs required by Annex A.1.2 and A.1.4.
2
3use crate::Xades;
4use structured_asn1::memory::{MemoryError, OwnedVec, Work};
5use structured_ltv::material::{
6    ArtifactRef, ArtifactUses,
7    status::{self, StatusPart},
8};
9use structured_ltv::pki::{Participant, ParticipantAuthority, PathStatuses};
10
11/// One original source contributing to a non-anchor checked-path status result.
12pub struct PathStatusSource<'certificate, 'document> {
13    pub(super) artifact: ArtifactRef<'certificate, 'document>,
14    member: usize,
15    contributor: usize,
16}
17impl<'c, 'd> PathStatusSource<'c, 'd> {
18    /// Exact complete/delta CRL or authenticated basic OCSP response.
19    /// The latter still needs its original whole envelope for digest comparison.
20    #[must_use]
21    pub fn artifact(&self) -> ArtifactRef<'c, 'd> {
22        self.artifact
23    }
24    /// Actual non-anchor member in issuer-to-subject order.
25    #[must_use]
26    pub fn member(&self) -> usize {
27        self.member
28    }
29    /// Contributing reason-source ordinal (zero for a single OCSP response).
30    #[must_use]
31    pub fn contributor(&self) -> usize {
32        self.contributor
33    }
34}
35
36pub(super) struct Required<'c, 'd, W: Work> {
37    pub(super) sources: OwnedVec<PathStatusSource<'c, 'd>, W>,
38    pub(super) missing_input: bool,
39}
40pub(super) fn collect<'c, 'd, N: Copy, D: Copy, G: Copy, W: Work>(
41    artifacts: &ArtifactUses<'_, '_, '_, 'c, 'd, N, D, G, Xades>,
42    work: &W,
43) -> Result<Required<'c, 'd, W>, MemoryError> {
44    work.charge_work(1)?;
45    let inputs = artifacts.inputs();
46    let root = inputs
47        .requirements()
48        .entries()
49        .first()
50        .expect("constructed signer requirement");
51    assert!(
52        matches!(root.participant(), Participant::Signer(_)),
53        "signer is first"
54    );
55    let mut sources = work.vector();
56    let mut missing_input = false;
57    if let ParticipantAuthority::Path(path) = root.authority() {
58        let statuses = inputs
59            .authorities()
60            .roots()
61            .first()
62            .expect("signer status root");
63        assert!(
64            core::ptr::eq(statuses.path(), path),
65            "constructed root association"
66        );
67        missing_input = append_path(statuses, work, |source| sources.try_push(source))?;
68    }
69    Ok(Required {
70        sources,
71        missing_input,
72    })
73}
74
75// Both normative required sets enumerate exactly the same complete/delta and
76// OCSP source model, retaining negative readings and the non-anchor boundary.
77fn append_path<'c, 'd, W: Work>(
78    statuses: &PathStatuses<'_, 'c, 'd>,
79    work: &W,
80    mut record: impl FnMut(PathStatusSource<'c, 'd>) -> Result<(), MemoryError>,
81) -> Result<bool, MemoryError> {
82    let mut missing_input = false;
83    for (member, entry) in statuses.entries().iter().enumerate() {
84        work.charge_work(1)?;
85        let Some(source) = status::member_source(entry) else {
86            // A responder exemption is not a revocation value for a signature
87            // or credential obligation. Do not fabricate referenced material.
88            missing_input = true;
89            continue;
90        };
91        // EN319132-1 A.1.2(1–2) and the delta clause require every actual
92        // contributing complete/delta value. The anchor is not a member.
93        // https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
94        status::visit_artifacts_in(source, work, |artifact, contributor, part| {
95            if part != StatusPart::SigningCertificate {
96                record(PathStatusSource {
97                    artifact,
98                    member,
99                    contributor,
100                })?;
101            }
102            Ok::<_, MemoryError>(())
103        })?;
104    }
105    Ok(missing_input)
106}
107
108/// One actual credential participant's non-anchor status contributor.
109pub struct CredentialStatusSource<'c, 'd> {
110    participant: usize,
111    pub(super) source: PathStatusSource<'c, 'd>,
112    pub(super) ordinary: bool,
113    pub(super) attribute: bool,
114}
115impl<'c, 'd> CredentialStatusSource<'c, 'd> {
116    /// Original credential/authority ordinal in the complete participant census.
117    #[must_use]
118    pub fn participant(&self) -> usize {
119        self.participant
120    }
121    /// Actual member/contributor and original binary artifact.
122    #[must_use]
123    pub fn source(&self) -> &PathStatusSource<'c, 'd> {
124        &self.source
125    }
126    /// Matching ordinary and attribute reference presence, respectively.
127    #[must_use]
128    pub fn presence(&self) -> (bool, bool) {
129        (self.ordinary, self.attribute)
130    }
131    /// A.1.4's SHOULD NOT duplication advisory, not an unconditional rejection.
132    #[must_use]
133    pub fn redundant_attribute_reference(&self) -> bool {
134        self.ordinary && self.attribute
135    }
136}
137
138pub(super) struct CredentialRequired<'c, 'd, W: Work> {
139    pub(super) sources: OwnedVec<CredentialStatusSource<'c, 'd>, W>,
140    pub(super) missing_input: bool,
141}
142
143pub(super) fn collect_credentials<'c, 'd, N: Copy, D: Copy, G: Copy, W: Work>(
144    artifacts: &ArtifactUses<'_, '_, '_, 'c, 'd, N, D, G, Xades>,
145    work: &W,
146) -> Result<CredentialRequired<'c, 'd, W>, MemoryError> {
147    let inputs = artifacts.inputs();
148    let mut roots = inputs.authorities().roots().iter();
149    let mut sources = work.vector();
150    let mut missing_input = false;
151    for (participant, entry) in inputs.requirements().entries().iter().enumerate() {
152        work.charge_work(1)?;
153        let ParticipantAuthority::Path(path) = entry.authority() else {
154            continue;
155        };
156        // There is one status root per PATH entry, not per participant. Direct
157        // anchors and certificate-free credential authorities have no lookup.
158        let statuses = roots
159            .next()
160            .expect("constructed complete path/status pairing");
161        assert!(
162            core::ptr::eq(statuses.path(), path),
163            "same constructed path receipt"
164        );
165        if !matches!(entry.participant(), Participant::Credential { .. }) {
166            continue;
167        }
168        // EN319132-1 A.1.4(1–2), including its complete/delta contributor clause.
169        // https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
170        missing_input |= append_path(statuses, work, |source| {
171            sources.try_push(CredentialStatusSource {
172                participant,
173                source,
174                ordinary: false,
175                attribute: false,
176            })
177        })?;
178    }
179    Ok(CredentialRequired {
180        sources,
181        missing_input,
182    })
183}