Skip to main content

structured_xades/
unsigned.rs

1//! Complete unsigned-property inventory and requested-profile presence rules.
2
3use crate::binding::Bound;
4use crate::container::XADES;
5use crate::properties::{Error, PropertyResult};
6use crate::timestamp::XADES_141;
7use crate::xml::{Access, AccessError, Name, Snapshot, Xml};
8use structured_asn1::memory::{OwnedVec, Work};
9
10/// Requested structural requirements, never an assertion of an attained level.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Requirements {
13    /// B-B requirements; presence of additional material does not promote level.
14    Basic,
15    /// B-T also requires at least one signature timestamp container.
16    Timestamped,
17    /// B-LT forbids reference-only validation substitutes and requires full data.
18    LongTerm,
19    /// B-LTA additionally requires an archive timestamp container.
20    Archival,
21}
22
23/// Known role of an original unsigned property; inner semantics remain separate.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum Role {
26    /// Enveloped child signature.
27    Counter,
28    /// Signature-value timestamp.
29    SignatureTimestamp,
30    /// Certificate values.
31    Certificates,
32    /// Revocation values.
33    Revocation,
34    /// Attribute-authority certificate values.
35    AttributeCertificates,
36    /// Attribute/assertion revocation values.
37    AttributeRevocation,
38    /// General validation material.
39    AnyValidation,
40    /// Timestamp-specific validation material.
41    TimestampValidation,
42    /// Current archive timestamp.
43    Archive,
44    /// Recomputed indirectly signed object digests.
45    RenewedDigests,
46    /// Referenced signature-policy document.
47    PolicyStore,
48    /// Complete certificate references, without values.
49    CertificateReferences,
50    /// Complete revocation references, without values.
51    RevocationReferences,
52    /// Attribute certificate references, without values.
53    AttributeCertificateReferences,
54    /// Attribute revocation references, without values.
55    AttributeRevocationReferences,
56    /// Timestamp over signature and validation references.
57    SignatureAndReferencesTimestamp,
58    /// Timestamp over validation references.
59    ReferencesTimestamp,
60    /// Schema-permitted foreign property, requiring explicit processing.
61    Extension,
62}
63
64/// One original entry in document order, retaining unknown extension identity.
65#[derive(Debug, Clone, Copy)]
66pub struct Entry<N> {
67    /// Original XML node.
68    pub node: N,
69    /// Known schema role, not an authentication or completeness result.
70    pub role: Role,
71}
72
73/// Full inventory. It cannot claim path/status validity or an attained B level.
74#[derive(Debug)]
75pub struct Catalogue<N, D, G> {
76    snapshot: Snapshot<D, G>,
77    signature: N,
78    parent: Option<N>,
79    data: Option<N>,
80    entries: OwnedVec<Entry<N>>,
81}
82
83impl<N: Copy, D: Copy, G: Copy> Catalogue<N, D, G> {
84    /// Original XML document and generation.
85    #[must_use]
86    pub fn snapshot(&self) -> Snapshot<D, G> {
87        self.snapshot
88    }
89    /// Selected signature whose unsigned properties were traversed.
90    #[must_use]
91    pub fn signature(&self) -> N {
92        self.signature
93    }
94    /// Present original unsigned-signature-properties container.
95    #[must_use]
96    pub fn parent(&self) -> Option<N> {
97        self.parent
98    }
99    /// Original optional unsigned-data-object container, kept independently of
100    /// signature properties; its application payload is not an `XAdES` verdict.
101    #[must_use]
102    pub fn data_objects(&self) -> Option<N> {
103        self.data
104    }
105    /// All original entries, preserving order for archive coverage.
106    #[must_use]
107    pub fn entries(&self) -> &[Entry<N>] {
108        &self.entries
109    }
110}
111
112/// Inventory using the XML owner's identity types.
113pub type Reading<X> = Catalogue<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>;
114
115/// Checks the credential-presence condition of §6.3(r/s/w). Self-claimed roles
116/// alone do not permit attribute-authority certificate/status material. A parsed
117/// credential permits the structural property, but its issuer authentication,
118/// path, status and asserted role must still be checked independently.
119pub fn check_credential_material<X: Xml, W: Work>(
120    access: &Access<'_, X, W>,
121    inventory: &Reading<X>,
122    roles: Option<&crate::role::Catalogue<X>>,
123) -> PropertyResult<(), X> {
124    if access.snapshot() != inventory.snapshot {
125        return Err(Error::Access(AccessError::Changed {
126            node: inventory.signature,
127        }));
128    }
129    let mut credential = false;
130    if let Some(roles) = roles {
131        if roles.snapshot() != inventory.snapshot
132            || !access
133                .equal_id(roles.signature(), inventory.signature)
134                .map_err(|cause| {
135                    Error::Access(AccessError::Resource {
136                        node: roles.node(),
137                        cause,
138                    })
139                })?
140        {
141            return Err(Error::Access(AccessError::Changed { node: roles.node() }));
142        }
143        for index in 0..roles.entries().len() {
144            access
145                .query(roles.node(), |_, _, work| Ok(work.charge_work(1)))
146                .map_err(Error::Access)?
147                .map_err(|cause| Error::Resource {
148                    node: roles.node(),
149                    cause,
150                })?;
151            if roles.entries()[index].kind != crate::role::Kind::Claimed {
152                credential = true;
153                break;
154            }
155        }
156    }
157    for index in 0..inventory.entries.len() {
158        access
159            .query(inventory.signature, |_, _, work| Ok(work.charge_work(1)))
160            .map_err(Error::Access)?
161            .map_err(|cause| Error::Resource {
162                node: inventory.signature,
163                cause,
164            })?;
165        let entry = inventory.entries[index];
166        if !credential
167            && matches!(
168                entry.role,
169                Role::AttributeCertificates
170                    | Role::AttributeRevocation
171                    | Role::AttributeCertificateReferences
172                    | Role::AttributeRevocationReferences
173            )
174        {
175            return Err(Error::Forbidden { node: entry.node });
176        }
177    }
178    Ok(())
179}
180
181/// Inventories every original unsigned property before profile execution.
182/// Required containers and prohibited reference-only forms follow EN319132-1
183/// §6.3 table2. Their mere presence is insufficient: timestamp mathematics,
184/// signer/authority paths and conditional material completeness remain mandatory.
185/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
186pub fn read<X: Xml, W: Work>(
187    access: &Access<'_, X, W>,
188    bound: &Bound<X::Id, X::Document, X::Generation>,
189    requirements: Requirements,
190) -> PropertyResult<Reading<X>, X> {
191    let layout = bound.containers();
192    let signature = layout.signature();
193    if access.snapshot() != layout.snapshot() {
194        return Err(Error::Access(AccessError::Changed { node: signature }));
195    }
196    let [parent, data] = containers(access, layout.unsigned())?;
197    let mut entries = access
198        .query(signature, |_, _, work| Ok(work.vector()))
199        .map_err(Error::Access)?;
200    let mut singletons = [false; 9];
201    let mut signature_timestamp = false;
202    let mut archive_timestamp = false;
203    if let Some(parent) = parent {
204        for child in access.children(parent) {
205            let node = child.map_err(Error::Child)?;
206            let role = classify(access, node)?;
207            // Reference-only forms are not validation-material service options
208            // for B-LT/B-LTA, even if an XML schema accepts their syntax.
209            let singleton = match role {
210                Role::Certificates => Some(0),
211                Role::Revocation => Some(1),
212                Role::AttributeCertificates => Some(2),
213                Role::AttributeRevocation => Some(3),
214                Role::PolicyStore => Some(4),
215                Role::CertificateReferences => Some(5),
216                Role::RevocationReferences => Some(6),
217                Role::AttributeCertificateReferences => Some(7),
218                Role::AttributeRevocationReferences => Some(8),
219                _ => None,
220            };
221            if let Some(index) = singleton {
222                if singletons[index] {
223                    return Err(Error::Duplicate { node });
224                }
225                singletons[index] = true;
226            }
227            if matches!(
228                requirements,
229                Requirements::LongTerm | Requirements::Archival
230            ) && matches!(
231                role,
232                Role::CertificateReferences
233                    | Role::RevocationReferences
234                    | Role::AttributeCertificateReferences
235                    | Role::AttributeRevocationReferences
236                    | Role::SignatureAndReferencesTimestamp
237                    | Role::ReferencesTimestamp
238            ) {
239                return Err(Error::Forbidden { node });
240            }
241            signature_timestamp |= role == Role::SignatureTimestamp;
242            archive_timestamp |= role == Role::Archive;
243            entries
244                .try_push(Entry { node, role })
245                .map_err(|cause| Error::Resource { node, cause })?;
246        }
247        if entries.is_empty() {
248            return Err(Error::Missing {
249                parent,
250                local: "unsigned property",
251            });
252        }
253    }
254    if requirements != Requirements::Basic && !signature_timestamp {
255        return Err(Error::Missing {
256            parent: signature,
257            local: "SignatureTimeStamp",
258        });
259    }
260    if requirements == Requirements::Archival && !archive_timestamp {
261        return Err(Error::Missing {
262            parent: signature,
263            local: "ArchiveTimeStamp",
264        });
265    }
266    Ok(Catalogue {
267        snapshot: layout.snapshot(),
268        signature,
269        parent,
270        data,
271        entries: entries.into_owned(),
272    })
273}
274
275pub(crate) fn containers<X: Xml, W: Work>(
276    access: &Access<'_, X, W>,
277    unsigned: Option<X::Id>,
278) -> PropertyResult<[Option<X::Id>; 2], X> {
279    let mut fields = [None; 2];
280    let Some(unsigned) = unsigned else {
281        return Ok(fields);
282    };
283    for child in access.children(unsigned) {
284        let node = child.map_err(Error::Child)?;
285        if access
286            .has_name(
287                node,
288                Name {
289                    namespace: XADES,
290                    local: "UnsignedSignatureProperties",
291                },
292            )
293            .map_err(Error::Access)?
294        {
295            if fields[0].is_some() {
296                return Err(Error::Duplicate { node });
297            }
298            if fields[1].is_some() {
299                return Err(Error::Order { node });
300            }
301            fields[0] = Some(node);
302        } else if access
303            .has_name(
304                node,
305                Name {
306                    namespace: XADES,
307                    local: "UnsignedDataObjectProperties",
308                },
309            )
310            .map_err(Error::Access)?
311        {
312            if fields[1].is_some() {
313                return Err(Error::Duplicate { node });
314            }
315            fields[1] = Some(node);
316            let mut populated = false;
317            for child in access.children(node) {
318                let child = child.map_err(Error::Child)?;
319                if !access
320                    .has_name(
321                        child,
322                        Name {
323                            namespace: XADES,
324                            local: "UnsignedDataObjectProperty",
325                        },
326                    )
327                    .map_err(Error::Access)?
328                {
329                    return Err(Error::Unexpected { node: child });
330                }
331                populated = true;
332            }
333            if !populated {
334                return Err(Error::Missing {
335                    parent: node,
336                    local: "UnsignedDataObjectProperty",
337                });
338            }
339        } else {
340            return Err(Error::Unexpected { node });
341        }
342    }
343    Ok(fields)
344}
345
346fn classify<X: Xml, W: Work>(access: &Access<'_, X, W>, node: X::Id) -> PropertyResult<Role, X> {
347    for local in [
348        "CompleteCertificateRefs",
349        "AttributeCertificateRefs",
350        "SigAndRefsTimeStamp",
351        "RefsOnlyTimeStamp",
352        "ArchiveTimeStamp",
353    ] {
354        if access
355            .has_name(
356                node,
357                Name {
358                    namespace: XADES,
359                    local,
360                },
361            )
362            .map_err(Error::Access)?
363        {
364            return Err(Error::Forbidden { node });
365        }
366    }
367    for (namespace, local, role) in [
368        (XADES, "CounterSignature", Role::Counter),
369        (XADES, "SignatureTimeStamp", Role::SignatureTimestamp),
370        (XADES, "CertificateValues", Role::Certificates),
371        (XADES, "RevocationValues", Role::Revocation),
372        (
373            XADES,
374            "AttrAuthoritiesCertValues",
375            Role::AttributeCertificates,
376        ),
377        (
378            XADES,
379            "AttributeRevocationValues",
380            Role::AttributeRevocation,
381        ),
382        (XADES, "CompleteRevocationRefs", Role::RevocationReferences),
383        (
384            XADES,
385            "AttributeRevocationRefs",
386            Role::AttributeRevocationReferences,
387        ),
388        (
389            XADES_141,
390            "CompleteCertificateRefsV2",
391            Role::CertificateReferences,
392        ),
393        (
394            XADES_141,
395            "AttributeCertificateRefsV2",
396            Role::AttributeCertificateReferences,
397        ),
398        (
399            XADES_141,
400            "SigAndRefsTimeStampV2",
401            Role::SignatureAndReferencesTimestamp,
402        ),
403        (XADES_141, "RefsOnlyTimeStampV2", Role::ReferencesTimestamp),
404        (XADES_141, "AnyValidationData", Role::AnyValidation),
405        (
406            XADES_141,
407            "TimeStampValidationData",
408            Role::TimestampValidation,
409        ),
410        (XADES_141, "ArchiveTimeStamp", Role::Archive),
411        (XADES_141, "RenewedDigestsV2", Role::RenewedDigests),
412        (XADES_141, "SignaturePolicyStore", Role::PolicyStore),
413    ] {
414        if access
415            .has_name(node, Name { namespace, local })
416            .map_err(Error::Access)?
417        {
418            return Ok(role);
419        }
420    }
421    let foreign = access
422        .inspect(node, |value| {
423            let crate::xml::Node::Element(name) = value else {
424                return Ok(false);
425            };
426            if name.namespace.is_empty() {
427                return Ok(false);
428            }
429            Ok(!access.equal(name.namespace, XADES)?)
430        })
431        .map_err(Error::Access)?
432        .map_err(|cause| Error::Resource { node, cause })?;
433    if !foreign {
434        return Err(Error::Unexpected { node });
435    }
436    Ok(Role::Extension)
437}