Skip to main content

structured_xades/
properties.rs

1//! Bound original signed-property roles, before their scalar/PKI checks.
2
3use crate::binding::Bound;
4use crate::container::{DS, XADES};
5use crate::xml::{Access, AccessError, ChildError, Name, Node, Snapshot, Xml};
6use structured_asn1::memory::{MemoryError, OwnedVec, Work};
7
8mod claimed_time;
9pub use claimed_time::{
10    CalendarFact, CalendarValue, CalendarXml, ClaimedTime, ClaimedTimeBinding, ClaimedTimeError,
11    ClaimedTimeFailure, ClaimedTimeResult, DateTimeFact, DateTimeXml, check_time,
12};
13
14const SIGNATURE_PROPERTIES: &[&str] = &[
15    "SigningTime",
16    "SigningCertificateV2",
17    "SignaturePolicyIdentifier",
18    "SignatureProductionPlaceV2",
19    "SignerRoleV2",
20];
21const LEGACY_PROPERTIES: &[&str] = &[
22    "SigningCertificate",
23    "SignatureProductionPlace",
24    "SignerRole",
25];
26const OBJECT_PROPERTIES: &[&str] = &[
27    "DataObjectFormat",
28    "CommitmentTypeIndication",
29    "AllDataObjectsTimeStamp",
30    "IndividualDataObjectsTimeStamp",
31];
32
33/// Schema role of an authenticated original data-object property.
34/// Scalar contents, referenced object selection and timestamp checks are separate.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum DataObjectRole {
37    /// Description, identifier, MIME type and encoding of a referenced object.
38    Format,
39    /// Original commitment type and its selected data-object references.
40    Commitment,
41    /// Timestamp on all selected signature data objects.
42    AllTimestamp,
43    /// Timestamp on explicitly selected signature data objects.
44    IndividualTimestamp,
45    /// Original schema-permitted foreign-namespace property.
46    Extension,
47}
48
49/// One original property within the fully authenticated `SignedProperties`.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct DataObject<N> {
52    /// Original element, never a reconstructed property or URI string.
53    pub node: N,
54    /// Structural role; this does not assert the property's semantic validity.
55    pub role: DataObjectRole,
56}
57
58/// Complete structural data-object property catalogue in its original order.
59#[derive(Debug)]
60pub struct DataObjects<N> {
61    parent: N,
62    entries: OwnedVec<DataObject<N>>,
63}
64
65impl<N: Copy> DataObjects<N> {
66    /// Original `SignedDataObjectProperties` element.
67    #[must_use]
68    pub fn parent(&self) -> N {
69        self.parent
70    }
71
72    /// Original entries, all protected by the enclosing selected binding.
73    /// Taking this slice performs no traversal; later processing must be funded.
74    #[must_use]
75    pub fn entries(&self) -> &[DataObject<N>] {
76        &self.entries
77    }
78}
79
80/// Failure selecting the signed signature properties of this baseline.
81#[derive(Debug)]
82pub enum Error<E, N> {
83    /// Caller selected no property of the requested original schema role.
84    Selection {
85        /// Original signed property container.
86        parent: N,
87        /// Caller-provided data-object property ordinal.
88        index: usize,
89    },
90    /// Present simple content violates the named property's lexical contract.
91    Lexical {
92        /// Original value element; no physical XML byte offset is invented.
93        node: N,
94        /// Schema/property local name.
95        local: &'static str,
96    },
97    /// Original XML/source/current-operation refusal.
98    Access(AccessError<E, N>),
99    /// Original element-only container failure.
100    Child(ChildError<E, N>),
101    /// A required original property is absent.
102    Missing {
103        /// Original enclosing node.
104        parent: N,
105        /// Required property's local name.
106        local: &'static str,
107    },
108    /// A singleton property occurs more than once.
109    Duplicate {
110        /// Repeated original property.
111        node: N,
112    },
113    /// Original property is outside its schema sequence position.
114    Order {
115        /// Original out-of-order property.
116        node: N,
117    },
118    /// Legacy property explicitly excluded by the current baseline.
119    Forbidden {
120        /// Original excluded property.
121        node: N,
122    },
123    /// This namespace/name has no role in the container's schema.
124    Unexpected {
125        /// Original unexpected node.
126        node: N,
127    },
128    /// Extension metadata could not be retained before storage growth.
129    Resource {
130        /// Original extension node.
131        node: N,
132        /// Original allocation/work refusal.
133        cause: MemoryError,
134    },
135}
136
137/// Selected original roles authenticated by the bound signature's full coverage.
138///
139/// This is a structural reading, not a baseline-level assertion: `SigningTime`
140/// lexical validation, certificate digest binding, and optional property content
141/// checks remain necessary. Unknown foreign-namespace extensions are retained
142/// for explicit processing, never silently interpreted as known properties.
143pub struct Signed<N, D, G> {
144    snapshot: Snapshot<D, G>,
145    signature: N,
146    parent: N,
147    time: N,
148    certificate: N,
149    policy: Option<N>,
150    place: Option<N>,
151    role: Option<N>,
152    extensions: OwnedVec<N>,
153    objects: Option<DataObjects<N>>,
154}
155
156impl<N: Copy, D: Copy, G: Copy> Signed<N, D, G> {
157    /// Original source generation carrying all selected properties.
158    #[must_use]
159    pub fn snapshot(&self) -> Snapshot<D, G> {
160        self.snapshot
161    }
162    /// Exact selected signature authenticating these original properties.
163    #[must_use]
164    pub fn signature(&self) -> N {
165        self.signature
166    }
167    /// Original `SignedSignatureProperties` node.
168    #[must_use]
169    pub fn parent(&self) -> N {
170        self.parent
171    }
172    /// Original claimed `SigningTime`; no UTC/trust claim is made by this reading.
173    #[must_use]
174    pub fn signing_time(&self) -> N {
175        self.time
176    }
177    /// Original mandatory `SigningCertificateV2`, before its PKI binding check.
178    #[must_use]
179    pub fn signing_certificate(&self) -> N {
180        self.certificate
181    }
182    /// Original optional policy identifier, before policy-content validation.
183    #[must_use]
184    pub fn policy(&self) -> Option<N> {
185        self.policy
186    }
187    /// Original optional `SignatureProductionPlaceV2`.
188    #[must_use]
189    pub fn place(&self) -> Option<N> {
190        self.place
191    }
192    /// Original optional `SignerRoleV2`.
193    #[must_use]
194    pub fn role(&self) -> Option<N> {
195        self.role
196    }
197    /// Original foreign-namespace extension identities, in document order.
198    #[must_use]
199    pub fn extensions(&self) -> &[N] {
200        &self.extensions
201    }
202
203    /// Original optional signed data-object property catalogue. Empty and absent
204    /// remain distinct; no timestamp or referenced-object assertion is inferred.
205    #[must_use]
206    pub fn data_objects(&self) -> Option<&DataObjects<N>> {
207        self.objects.as_ref()
208    }
209}
210
211/// Reading using the selected XML owner's original identities.
212pub type Reading<X> = Signed<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>;
213/// Failure retaining original source and XML owner causes.
214pub type PropertyResult<T, X> = Result<T, Error<<X as Xml>::Error, <X as Xml>::Id>>;
215
216/// Rejects element children of a simple-typed property. XML character validity
217/// and normalization are already established by the original XML owner; this
218/// only enforces the schema's absence of element children, without collecting
219/// text, trimming whitespace or substituting scalar lexical validation.
220pub(crate) fn simple_content<X: Xml, W: Work>(
221    access: &Access<'_, X, W>,
222    parent: X::Id,
223) -> PropertyResult<(), X> {
224    let mut child = access.first_child(parent).map_err(Error::Access)?;
225    while let Some(node) = child {
226        if access
227            .inspect(node, |value| matches!(value, Node::Element(_)))
228            .map_err(Error::Access)?
229        {
230            return Err(Error::Unexpected { node });
231        }
232        child = access.next_sibling(parent, node).map_err(Error::Access)?;
233    }
234    Ok(())
235}
236
237/// The shared `DigestAlgAndValueType` grammar used by certificate and policy
238/// references. Selected algorithm parameters/base64 decoding belong to their
239/// existing owner kernels, not another XML/crypto parser here.
240pub(crate) fn digest_value<X: Xml, W: Work>(
241    access: &Access<'_, X, W>,
242    parent: X::Id,
243) -> PropertyResult<(X::Id, X::Id), X> {
244    let mut children = access.children(parent);
245    let method = children
246        .next()
247        .transpose()
248        .map_err(Error::Child)?
249        .ok_or(Error::Missing {
250            parent,
251            local: "DigestMethod",
252        })?;
253    if !access
254        .has_name(
255            method,
256            Name {
257                namespace: DS,
258                local: "DigestMethod",
259            },
260        )
261        .map_err(Error::Access)?
262    {
263        return Err(Error::Unexpected { node: method });
264    }
265    let value = children
266        .next()
267        .transpose()
268        .map_err(Error::Child)?
269        .ok_or(Error::Missing {
270            parent,
271            local: "DigestValue",
272        })?;
273    if !access
274        .has_name(
275            value,
276            Name {
277                namespace: DS,
278                local: "DigestValue",
279            },
280        )
281        .map_err(Error::Access)?
282    {
283        return Err(Error::Unexpected { node: value });
284    }
285    if let Some(extra) = children.next() {
286        return Err(Error::Unexpected {
287            node: extra.map_err(Error::Child)?,
288        });
289    }
290    simple_content(access, value)?;
291    Ok((method, value))
292}
293
294pub(crate) type Sections<X> = (Option<<X as Xml>::Id>, Option<<X as Xml>::Id>);
295
296// One structural SignedProperties field kernel. An incorporated counter's
297// credential census does not impose the root's current baseline requirements.
298pub(crate) fn sections<X: Xml, W: Work>(
299    access: &Access<'_, X, W>,
300    layout: &crate::container::Layout<X>,
301) -> PropertyResult<Sections<X>, X> {
302    if layout.snapshot() != access.snapshot() {
303        return Err(Error::Access(AccessError::Changed {
304            node: layout.signed(),
305        }));
306    }
307    let mut parent = None;
308    let mut objects = None;
309    for child in access.children(layout.signed()) {
310        let node = child.map_err(Error::Child)?;
311        if named(access, node, "SignedSignatureProperties")? {
312            if parent.is_some() {
313                return Err(Error::Duplicate { node });
314            }
315            if objects.is_some() {
316                return Err(Error::Order { node });
317            }
318            parent = Some(node);
319        } else if named(access, node, "SignedDataObjectProperties")? {
320            if objects.is_some() {
321                return Err(Error::Duplicate { node });
322            }
323            objects = Some(node);
324        } else {
325            return Err(Error::Unexpected { node });
326        }
327    }
328    Ok((parent, objects))
329}
330
331/// Selects the current baseline's original signed signature-property roles.
332///
333/// EN319132-1 V1.3.1 §6.3/Table2 requires `SigningTime` and `SigningCertificateV2`,
334/// and excludes their legacy certificate/place/role alternatives. The schema's
335/// sequence and singleton cardinalities still apply in addition to the table.
336/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
337pub fn read<X: Xml, W: Work>(
338    access: &Access<'_, X, W>,
339    bound: &Bound<X::Id, X::Document, X::Generation>,
340) -> PropertyResult<Reading<X>, X> {
341    let layout = bound.containers();
342    let (parent, objects) = sections(access, layout)?;
343    let parent = parent.ok_or(Error::Missing {
344        parent: layout.signed(),
345        local: "SignedSignatureProperties",
346    })?;
347    let mut fields = [None; 5];
348    let mut previous = 0;
349    let mut extensions = access
350        .query(parent, |_, _, work| Ok(work.vector()))
351        .map_err(Error::Access)?;
352    for child in access.children(parent) {
353        let node = child.map_err(Error::Child)?;
354        let kind = classify(access, node, SIGNATURE_PROPERTIES, LEGACY_PROPERTIES)?;
355        let rank = match kind {
356            Kind::Known(rank) => {
357                if fields[rank].is_some() {
358                    return Err(Error::Duplicate { node });
359                }
360                fields[rank] = Some(node);
361                rank
362            }
363            Kind::Extension => {
364                extensions
365                    .try_push(node)
366                    .map_err(|cause| Error::Resource { node, cause })?;
367                SIGNATURE_PROPERTIES.len()
368            }
369            Kind::Forbidden => return Err(Error::Forbidden { node }),
370            Kind::Unexpected => return Err(Error::Unexpected { node }),
371        };
372        if rank < previous {
373            return Err(Error::Order { node });
374        }
375        previous = rank;
376    }
377    let time = fields[0].ok_or(Error::Missing {
378        parent,
379        local: "SigningTime",
380    })?;
381    let certificate = fields[1].ok_or(Error::Missing {
382        parent,
383        local: "SigningCertificateV2",
384    })?;
385    simple_content(access, time)?;
386    if let Some(place) = fields[3] {
387        production_place(access, place)?;
388    }
389    let objects = objects
390        .map(|parent| read_objects(access, parent))
391        .transpose()?;
392    Ok(Signed {
393        snapshot: layout.snapshot(),
394        signature: layout.signature(),
395        parent,
396        time,
397        certificate,
398        policy: fields[2],
399        place: fields[3],
400        role: fields[4],
401        extensions: extensions.into_owned(),
402        objects,
403    })
404}
405
406fn production_place<X: Xml, W: Work>(
407    access: &Access<'_, X, W>,
408    parent: X::Id,
409) -> PropertyResult<(), X> {
410    // EN319132-1 V1.3.1 §5.2.5: V2 includes StreetAddress in this position,
411    // and an empty property must not be generated although the XSD permits it.
412    // https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
413    const FIELDS: &[&str] = &[
414        "City",
415        "StreetAddress",
416        "StateOrProvince",
417        "PostalCode",
418        "CountryName",
419    ];
420    let mut seen = [false; 5];
421    let mut previous = None;
422    for child in access.children(parent) {
423        let node = child.map_err(Error::Child)?;
424        let rank = match classify(access, node, FIELDS, &[])? {
425            Kind::Known(rank) => rank,
426            Kind::Forbidden | Kind::Unexpected | Kind::Extension => {
427                return Err(Error::Unexpected { node });
428            }
429        };
430        if seen[rank] {
431            return Err(Error::Duplicate { node });
432        }
433        if let Some(previous) = previous
434            && rank < previous
435        {
436            return Err(Error::Order { node });
437        }
438        seen[rank] = true;
439        previous = Some(rank);
440        simple_content(access, node)?;
441    }
442    if previous.is_none() {
443        return Err(Error::Missing {
444            parent,
445            local: "production-place field",
446        });
447    }
448    Ok(())
449}
450
451enum Kind {
452    Known(usize),
453    Extension,
454    Forbidden,
455    Unexpected,
456}
457
458fn classify<X: Xml, W: Work>(
459    access: &Access<'_, X, W>,
460    node: X::Id,
461    known: &[&str],
462    forbidden: &[&str],
463) -> PropertyResult<Kind, X> {
464    access
465        .inspect(node, |value| {
466            let Node::Element(actual) = value else {
467                return Ok(Kind::Unexpected);
468            };
469            // XSD 1.0 §3.10.4, Wildcard allows Namespace Name 2.3:
470            // ##other does not include an absent namespace.
471            // https://www.w3.org/TR/xmlschema-1/#cvc-wildcard-namespace
472            if actual.namespace.is_empty() {
473                return Ok(Kind::Unexpected);
474            }
475            if !access.equal(actual.namespace, XADES)? {
476                return Ok(Kind::Extension);
477            }
478            for (rank, local) in known.iter().enumerate() {
479                if access.equal(actual.local, local)? {
480                    return Ok(Kind::Known(rank));
481                }
482            }
483            for local in forbidden {
484                if access.equal(actual.local, local)? {
485                    return Ok(Kind::Forbidden);
486                }
487            }
488            Ok(Kind::Unexpected)
489        })
490        .map_err(Error::Access)?
491        .map_err(|cause| Error::Resource { node, cause })
492}
493
494pub(crate) fn read_objects<X: Xml, W: Work>(
495    access: &Access<'_, X, W>,
496    parent: X::Id,
497) -> PropertyResult<DataObjects<X::Id>, X> {
498    // EN319132-1 V1.3.1 Annex C: each known sequence member is optional and
499    // repeatable; foreign ##other properties follow every known member.
500    // https://forge.etsi.org/rep/esi/x19_13201_XAdES/raw/v1.3.1/1913201-XAdES01903v132.xsd
501    let mut entries = access
502        .query(parent, |_, _, work| Ok(work.vector()))
503        .map_err(Error::Access)?;
504    let mut previous = 0;
505    for child in access.children(parent) {
506        let node = child.map_err(Error::Child)?;
507        let (rank, role) = match classify(access, node, OBJECT_PROPERTIES, &[])? {
508            Kind::Known(rank) => {
509                let roles = [
510                    DataObjectRole::Format,
511                    DataObjectRole::Commitment,
512                    DataObjectRole::AllTimestamp,
513                    DataObjectRole::IndividualTimestamp,
514                ];
515                debug_assert!(rank < roles.len());
516                (rank, roles[rank])
517            }
518            Kind::Extension => (OBJECT_PROPERTIES.len(), DataObjectRole::Extension),
519            Kind::Forbidden | Kind::Unexpected => return Err(Error::Unexpected { node }),
520        };
521        if rank < previous {
522            return Err(Error::Order { node });
523        }
524        entries
525            .try_push(DataObject { node, role })
526            .map_err(|cause| Error::Resource { node, cause })?;
527        previous = rank;
528    }
529    Ok(DataObjects {
530        parent,
531        entries: entries.into_owned(),
532    })
533}
534
535fn named<X: Xml, W: Work>(
536    access: &Access<'_, X, W>,
537    node: X::Id,
538    local: &str,
539) -> PropertyResult<bool, X> {
540    access
541        .has_name(
542            node,
543            Name {
544                namespace: XADES,
545                local,
546            },
547        )
548        .map_err(Error::Access)
549}