Skip to main content

structured_xades/material/
catalogue.rs

1//! Structural material catalogues; completeness is established from actual paths.
2
3use super::Kind;
4use crate::container::XADES;
5use crate::properties::{Error, PropertyResult};
6use crate::xml::{Access, Name, Snapshot, Xml};
7use structured_asn1::memory::{OwnedVec, Work};
8
9/// One original value in a validation-material property, in document order.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Entry<N> {
12    /// A specified ASN.1 material encoding, not yet parsed or authenticated.
13    Encapsulated {
14        /// Original encapsulated-value node.
15        node: N,
16        /// Actual PKI schema specified for that node.
17        kind: Kind,
18    },
19    /// Schema-permitted non-X509 certificate. Presence cannot satisfy a required
20    /// X509 path certificate merely by being counted as a material entry.
21    OtherCertificate {
22        /// Original opaque `OtherCertificate` element.
23        node: N,
24    },
25    /// Schema-permitted other status format. It is not a CRL or OCSP response
26    /// without an explicit qualified format recognizer and authentication path.
27    OtherStatus {
28        /// Original opaque `OtherValue` element.
29        node: N,
30    },
31}
32
33/// Original complete structural catalogue, separate from material validation.
34#[derive(Debug)]
35pub struct Catalogue<N, D, G> {
36    snapshot: Snapshot<D, G>,
37    parent: N,
38    entries: OwnedVec<Entry<N>>,
39}
40
41impl<N: Copy, D: Copy, G: Copy> Catalogue<N, D, G> {
42    /// Original XML document/generation.
43    #[must_use]
44    pub fn snapshot(&self) -> Snapshot<D, G> {
45        self.snapshot
46    }
47
48    /// Original certificate/revocation-values element, including attribute roles.
49    #[must_use]
50    pub fn parent(&self) -> N {
51        self.parent
52    }
53
54    /// Original entries in document order. Obtaining the slice performs no scan;
55    /// consumers must fund their subsequent data-dependent processing.
56    #[must_use]
57    pub fn entries(&self) -> &[Entry<N>] {
58        &self.entries
59    }
60}
61
62/// Catalogue using the XML owner's source identities.
63pub type Reading<X> = Catalogue<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>;
64
65// Original material roles shared by retention and conditional-copy checks.
66// This is not a baseline inventory: plain/older counters do not acquire the
67// root's B-level property prohibitions merely by carrying validation values.
68#[derive(Clone, Copy)]
69pub(super) enum ContainerKind {
70    Certificates,
71    Revocation,
72    AttributeCertificates,
73    AttributeRevocation,
74}
75
76impl ContainerKind {
77    pub(super) fn attribute(self) -> bool {
78        matches!(
79            self,
80            Self::AttributeCertificates | Self::AttributeRevocation
81        )
82    }
83    pub(super) fn certificates(self) -> bool {
84        matches!(self, Self::Certificates | Self::AttributeCertificates)
85    }
86    pub(super) fn inventory(role: crate::unsigned::Role) -> Option<Self> {
87        match role {
88            crate::unsigned::Role::Certificates => Some(Self::Certificates),
89            crate::unsigned::Role::Revocation => Some(Self::Revocation),
90            crate::unsigned::Role::AttributeCertificates => Some(Self::AttributeCertificates),
91            crate::unsigned::Role::AttributeRevocation => Some(Self::AttributeRevocation),
92            _ => None,
93        }
94    }
95}
96
97pub(super) fn container_kind<X: Xml, W: Work>(
98    access: &Access<'_, X, W>,
99    node: X::Id,
100) -> PropertyResult<Option<ContainerKind>, X> {
101    for (local, kind) in [
102        ("CertificateValues", ContainerKind::Certificates),
103        ("RevocationValues", ContainerKind::Revocation),
104        (
105            "AttrAuthoritiesCertValues",
106            ContainerKind::AttributeCertificates,
107        ),
108        (
109            "AttributeRevocationValues",
110            ContainerKind::AttributeRevocation,
111        ),
112    ] {
113        if named(access, node, local)? {
114            return Ok(Some(kind));
115        }
116    }
117    Ok(None)
118}
119
120/// Reads one complete material property, without decoding or provider side effects.
121///
122/// EN319132-1 V1.3.1 Annex C's normative schema permits an empty
123/// `CertificateValues` choice and an empty `RevocationValues` sequence. A present
124/// CRL/OCSP/other-status group instead requires at least one member. These schema
125/// facts do not prove or disprove the conditional B-LT material completeness.
126/// <https://forge.etsi.org/rep/esi/x19_13201_XAdES/raw/v1.3.1/1913201-XAdES01903v132.xsd>
127pub fn read<X: Xml, W: Work>(
128    access: &Access<'_, X, W>,
129    parent: X::Id,
130) -> PropertyResult<Reading<X>, X> {
131    // Annex C gives attribute-authority containers the SAME value types.
132    // Their conditional role/authentication rules remain with the profile,
133    // not a second certificate/revocation grammar here.
134    let kind = container_kind(access, parent)?.ok_or(Error::Unexpected { node: parent })?;
135    let certificates = kind.certificates();
136    let mut entries = access
137        .query(parent, |_, _, work| Ok(work.vector()))
138        .map_err(Error::Access)?;
139    if certificates {
140        for child in access.children(parent) {
141            let node = child.map_err(Error::Child)?;
142            let entry = if named(access, node, "EncapsulatedX509Certificate")? {
143                Entry::Encapsulated {
144                    node,
145                    kind: Kind::Certificate,
146                }
147            } else if named(access, node, "OtherCertificate")? {
148                Entry::OtherCertificate { node }
149            } else {
150                return Err(Error::Unexpected { node });
151            };
152            entries
153                .try_push(entry)
154                .map_err(|cause| Error::Resource { node, cause })?;
155        }
156    } else {
157        let mut previous = None;
158        for child in access.children(parent) {
159            let group = child.map_err(Error::Child)?;
160            let (rank, member, kind) = if named(access, group, "CRLValues")? {
161                (0, "EncapsulatedCRLValue", Some(Kind::Crl))
162            } else if named(access, group, "OCSPValues")? {
163                (1, "EncapsulatedOCSPValue", Some(Kind::Ocsp))
164            } else if named(access, group, "OtherValues")? {
165                (2, "OtherValue", None)
166            } else {
167                return Err(Error::Unexpected { node: group });
168            };
169            if let Some(earlier) = previous {
170                if rank == earlier {
171                    return Err(Error::Duplicate { node: group });
172                }
173                if rank < earlier {
174                    return Err(Error::Order { node: group });
175                }
176            }
177            previous = Some(rank);
178            let mut present = false;
179            for child in access.children(group) {
180                let node = child.map_err(Error::Child)?;
181                if !named(access, node, member)? {
182                    return Err(Error::Unexpected { node });
183                }
184                let entry = match kind {
185                    Some(kind) => Entry::Encapsulated { node, kind },
186                    None => Entry::OtherStatus { node },
187                };
188                entries
189                    .try_push(entry)
190                    .map_err(|cause| Error::Resource { node, cause })?;
191                present = true;
192            }
193            if !present {
194                return Err(Error::Missing {
195                    parent: group,
196                    local: member,
197                });
198            }
199        }
200    }
201    Ok(Catalogue {
202        snapshot: access.snapshot(),
203        parent,
204        entries: entries.into_owned(),
205    })
206}
207
208fn named<X: Xml, W: Work>(
209    access: &Access<'_, X, W>,
210    node: X::Id,
211    local: &'static str,
212) -> PropertyResult<bool, X> {
213    access
214        .has_name(
215            node,
216            Name {
217                namespace: XADES,
218                local,
219            },
220        )
221        .map_err(Error::Access)
222}