Skip to main content

structured_xades/
attributes.rs

1//! Original `XAdES` attribute declarations and their base-datatype obligations.
2
3mod declaration;
4mod types;
5
6use crate::container::{Layout, XADES};
7use crate::pki::CounterPaths;
8use crate::prepare::Prepared;
9use crate::reference::{LexicalSource, VisitError};
10use crate::scalar::{self, Datatype, QNameFact, QNameXml, ScalarXml};
11use crate::timestamp::XADES_141;
12use crate::xml::{Access, AccessError, Name, Node, Snapshot, Xml};
13use structured_asn1::memory::{MemoryError, Work};
14
15/// Original direct attributes, excluding namespace declarations. Their names
16/// remain scoped to the visitor; no copied attribute catalogue is required.
17pub trait AttributeXml: Xml + QNameXml {
18    /// Visit each original direct attribute once, preserving first callback
19    /// refusal. The owner checks document/generation and admits traversal/name
20    /// work through THIS execution before access. XML normalization and namespace
21    /// expansion belong to that owner; unqualified attributes have no namespace.
22    fn visit_attributes<VE, W: Work>(
23        &self,
24        snapshot: Snapshot<Self::Document, Self::Generation>,
25        node: Self::Id,
26        execution: &W,
27        visitor: impl for<'view> FnMut(Name<'view>) -> Result<(), VE>,
28    ) -> Result<(), VisitError<Self::Error, VE>>;
29}
30
31/// Visits original attribute names with the common snapshot/admission fence.
32/// Format-specific declarations belong to the requesting schema; traversal
33/// and temporary namespace/name storage remain with the XML owner.
34pub fn visit<X: AttributeXml, W: Work, E>(
35    access: &Access<'_, X, W>,
36    node: X::Id,
37    visitor: impl for<'view> FnMut(Name<'view>) -> Result<(), E>,
38) -> crate::xml::VisitResult<(), X, E> {
39    access.query_fallible(node, |xml, snapshot, work| {
40        xml.visit_attributes(snapshot, node, work, visitor)
41    })
42}
43
44/// Schema-instance attributes need their own original interpretation; they are
45/// not ordinary undeclared attributes or a request to fetch a schema.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum InstanceAttribute {
48    /// An overriding schema type or unavailable `QName` projection is unassessed.
49    Type,
50    /// Original schema-location hints have not been assessed. No implicit fetch.
51    SchemaLocation,
52}
53
54/// Known original xsi:type reading failure, not an unavailable type definition.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum TypeProblem {
57    /// Owner did not return the attribute observed by the same source traversal.
58    Absent,
59    /// Original source has element children rather than simple content.
60    ComplexContent,
61    /// Original text fails XSD1.0 `QName` syntax.
62    Malformed,
63    /// Original lexical prefix has no in-scope namespace binding.
64    UnboundPrefix,
65}
66
67/// Original declaration, lexical, source and resource refusals stay distinct.
68#[derive(Debug)]
69pub enum Error<E, N> {
70    /// The supplied retained source belongs to another original selection.
71    Foreign,
72    /// Original XML/generation/current-operation refusal.
73    Access(AccessError<E, N>),
74    /// A requested original ID/URI/integer value failed or is unsupported.
75    Scalar(scalar::Error<E, N>),
76    /// This original attribute has no declaration at the selected field.
77    Unexpected {
78        /// Original owning element.
79        node: N,
80        /// Zero-based direct-attribute ordinal in that immutable view, not a byte offset.
81        attribute: usize,
82    },
83    /// A required original unqualified attribute is absent.
84    Missing {
85        /// Original owning element.
86        node: N,
87        /// Missing wire name, never an inferred/default value.
88        local: &'static str,
89    },
90    /// This is incomplete schema-instance processing, NOT malformed syntax.
91    InstanceUnassessed {
92        /// Original owning element.
93        node: N,
94        /// Exact original attribute role requiring interpretation.
95        kind: InstanceAttribute,
96    },
97    /// A known `QName` reading failure at the original xsi:type attribute.
98    Type {
99        /// Original owning element, permitting inspection of the original text.
100        node: N,
101        /// Exact reading-failure category; no callback-local prefix is retained.
102        problem: TypeProblem,
103    },
104    /// The original `XAdES` node has no selected declaration in this capability.
105    Unassessed {
106        /// Original element, not an automatically accepted unknown field.
107        node: N,
108    },
109}
110
111/// Result in this XML owner's original source/error domain.
112pub type AttributeResult<X> = Result<(), Error<<X as Xml>::Error, <X as Xml>::Id>>;
113
114#[derive(Clone, Copy)]
115enum Rule {
116    None,
117    Id,
118    Target,
119    Encoding,
120    Uri,
121    IdUri,
122    RequiredUri,
123    Include,
124    Format,
125    Identifier,
126    Order,
127    Open,
128}
129
130#[derive(Clone, Copy)]
131enum Context {
132    Ordinary,
133    Renewed,
134    CertifiedV2,
135    CertV2,
136}
137
138struct Declaration {
139    rule: Rule,
140    descend: bool,
141    context: Context,
142    schema_type: Name<'static>,
143}
144
145#[derive(Clone, Copy)]
146enum Field {
147    Typed(Datatype),
148    Required(Datatype),
149    Declared,
150}
151
152fn field(rule: Rule, local: &str) -> Option<Field> {
153    Some(match (rule, local) {
154        (Rule::Id | Rule::Target | Rule::Encoding | Rule::IdUri | Rule::RequiredUri, "Id") => {
155            Field::Typed(Datatype::Id)
156        }
157        (Rule::Target, "Target")
158        | (Rule::Format, "ObjectReference")
159        | (Rule::Include | Rule::RequiredUri, "URI") => Field::Required(Datatype::AnyUri),
160        (Rule::Encoding, "Encoding") | (Rule::Uri | Rule::IdUri, "URI") => {
161            Field::Typed(Datatype::AnyUri)
162        }
163        (Rule::Order, "Order") => Field::Required(Datatype::Integer),
164        (Rule::Identifier, "Qualifier") | (Rule::Include, "referencedData") => Field::Declared,
165        _ => return None,
166    })
167}
168
169use self::declaration::declaration;
170
171/// Check the selected root's original declared attributes BEFORE path/provider
172/// execution. This needs only structural preparation, not a successful PKIX
173/// path or countersignature result. Child containers are checked separately by
174/// [`check`]; no root-only result certifies all participants or a baseline level.
175pub fn check_prepared<X: AttributeXml + ScalarXml, W: Work>(
176    prepared: &Prepared<X>,
177    access: &Access<'_, X, W>,
178) -> AttributeResult<X> {
179    let layout = prepared.bound().containers();
180    if layout.snapshot() != access.snapshot() {
181        return Err(Error::Foreign);
182    }
183    check_layout(layout, access)?;
184    access
185        .query(layout.signature(), |_, _, _| Ok(()))
186        .map_err(Error::Access)
187}
188
189/// Check declared attribute names and requested ID/URI/integer values in the
190/// original selected `XAdES` source and ALL original counter containers. Open
191/// foreign/AnyType payloads are delegated, never recursively reinterpreted by
192/// their local names. `XMLDSig` elements retain their XML owner's contract.
193///
194/// This is NOT whole-schema validity or a B level: child grammar, schema-instance
195/// interpretation, OID meaning, binary values, dates, math, trust and complete
196/// profile assembly remain independent obligations. Unassessed xsi attributes
197/// never become successful syntax or a fabricated malformed-document finding.
198pub fn check<X: AttributeXml + ScalarXml, W: Work>(
199    prepared: &Prepared<X>,
200    counters: &CounterPaths<'_, '_, X::Id, X::Document, X::Generation>,
201    access: &Access<'_, X, W>,
202) -> AttributeResult<X> {
203    require_participants(prepared, counters, access)?;
204    check_prepared(prepared, access)?;
205    check_counter_nodes(prepared, counters, access)
206}
207
208fn require_participants<X: AttributeXml + ScalarXml, W: Work>(
209    prepared: &Prepared<X>,
210    counters: &CounterPaths<'_, '_, X::Id, X::Document, X::Generation>,
211    access: &Access<'_, X, W>,
212) -> AttributeResult<X> {
213    let layout = prepared.bound().containers();
214    if layout.snapshot() != access.snapshot()
215        || counters.snapshot() != access.snapshot()
216        || !access
217            .equal_id(layout.signature(), counters.signature())
218            .map_err(|cause| resource(layout.signature(), cause))?
219    {
220        return Err(Error::Foreign);
221    }
222    Ok(())
223}
224
225// The final root assembly already holds its checked Fields receipt. Reuse
226// counter traversal without repeating the root's scalar/attribute preflight.
227pub(crate) fn check_counters<X: AttributeXml + ScalarXml, W: Work>(
228    prepared: &Prepared<X>,
229    counters: &CounterPaths<'_, '_, X::Id, X::Document, X::Generation>,
230    access: &Access<'_, X, W>,
231) -> AttributeResult<X> {
232    require_participants(prepared, counters, access)?;
233    check_counter_nodes(prepared, counters, access)
234}
235
236fn check_counter_nodes<X: AttributeXml + ScalarXml, W: Work>(
237    prepared: &Prepared<X>,
238    counters: &CounterPaths<'_, '_, X::Id, X::Document, X::Generation>,
239    access: &Access<'_, X, W>,
240) -> AttributeResult<X> {
241    let layout = prepared.bound().containers();
242    for actor in counters.actors() {
243        access
244            .execution()
245            .charge_work(1)
246            .map_err(|cause| resource(layout.signature(), cause))?;
247        if let Some(layout) = actor.containers() {
248            check_layout(layout, access)?;
249        }
250    }
251    access
252        .query(layout.signature(), |_, _, _| Ok(()))
253        .map_err(Error::Access)
254}
255
256struct Frame<N> {
257    parent: N,
258    next: Option<N>,
259    context: Context,
260}
261
262fn check_layout<X: AttributeXml + ScalarXml, W: Work>(
263    layout: &Layout<X>,
264    access: &Access<'_, X, W>,
265) -> AttributeResult<X> {
266    let mut frames = access.execution().vector();
267    let mut next = Some((layout.qualifying(), Context::Ordinary));
268    loop {
269        if let Some((node, parent)) = next.take() {
270            let selected = access
271                .inspect(node, |value| {
272                    let Node::Element(name) = value else {
273                        return Ok(None);
274                    };
275                    let length = name
276                        .namespace
277                        .len()
278                        .checked_add(name.local.len())
279                        .and_then(|cost| u64::try_from(cost).ok())
280                        .ok_or(MemoryError::WorkExceeded)?;
281                    access.execution().charge_work(length)?;
282                    if name.namespace != XADES && name.namespace != XADES_141 {
283                        return Ok(None);
284                    }
285                    Ok(Some(declaration(name, parent)))
286                })
287                .map_err(Error::Access)?
288                .map_err(|cause| resource(node, cause))?;
289            if let Some(selected) = selected {
290                let selected = selected.ok_or(Error::Unassessed { node })?;
291                check_node(node, selected.rule, selected.schema_type, access)?;
292                if selected.descend {
293                    let child = access.first_child(node).map_err(Error::Access)?;
294                    frames
295                        .try_push(Frame {
296                            parent: node,
297                            next: child,
298                            context: selected.context,
299                        })
300                        .map_err(|cause| resource(node, cause))?;
301                }
302            }
303        }
304        access
305            .execution()
306            .charge_work(1)
307            .map_err(|cause| resource(layout.qualifying(), cause))?;
308        let Some(frame) = frames.last_mut() else {
309            break;
310        };
311        if let Some(child) = frame.next {
312            frame.next = access
313                .next_sibling(frame.parent, child)
314                .map_err(Error::Access)?;
315            next = Some((child, frame.context));
316        } else {
317            frames.pop();
318        }
319    }
320    Ok(())
321}
322
323fn resource<E, N>(node: N, cause: MemoryError) -> Error<E, N> {
324    Error::Access(AccessError::Resource { node, cause })
325}
326
327fn check_node<X: AttributeXml + ScalarXml, W: Work>(
328    node: X::Id,
329    rule: Rule,
330    schema_type: Name<'static>,
331    access: &Access<'_, X, W>,
332) -> AttributeResult<X> {
333    let mut ordinal = 0usize;
334    let mut required = false;
335    let visited = access.query_fallible(node, |xml, snapshot, work| {
336        xml.visit_attributes(snapshot, node, work, |name| {
337            let cost = u64::try_from(name.local.len())
338                .ok()
339                .and_then(|length| length.checked_add(1))
340                .ok_or_else(|| resource(node, MemoryError::WorkExceeded))?;
341            work.charge_work(cost)
342                .map_err(|cause| resource(node, cause))?;
343            let current = ordinal;
344            ordinal = ordinal
345                .checked_add(1)
346                .ok_or_else(|| resource(node, MemoryError::WorkExceeded))?;
347            if access
348                .equal(name.namespace, "http://www.w3.org/2001/XMLSchema-instance")
349                .map_err(|cause| resource(node, cause))?
350            {
351                // XSD1.0 Structures §3.3.4(3.1): non-nillable declarations
352                // forbid xsi:nil's PRESENCE, including nil="false". Every
353                // element declared by the pinned Annex C schemas omits
354                // nillable (default false), including AnyType enclosures.
355                // Foreign/open CONTENT remains with its format owner.
356                // https://www.w3.org/TR/2004/REC-xmlschema-1-20041028/#cvc-elt
357                if name.local == "nil" {
358                    return Err(Error::Unexpected {
359                        node,
360                        attribute: current,
361                    });
362                }
363                if name.local == "noNamespaceSchemaLocation" {
364                    // XSD1.0 Structures §3.2.7 declares exactly anyURI. The
365                    // hint cannot replace our pinned declaration or initiate
366                    // acquisition (§4.3.2); use the original XML scalar owner.
367                    // https://www.w3.org/TR/2004/REC-xmlschema-1-20041028/#BuiltInAttrDecls
368                    return scalar::check(
369                        access,
370                        node,
371                        LexicalSource::Attribute(name),
372                        Datatype::AnyUri,
373                    )
374                    .map_err(Error::Scalar);
375                }
376                let kind = match name.local {
377                    "type" => Some(InstanceAttribute::Type),
378                    "schemaLocation" => Some(InstanceAttribute::SchemaLocation),
379                    _ => None,
380                };
381                if let Some(kind) = kind {
382                    if matches!(kind, InstanceAttribute::Type)
383                        && same_declared_type(node, name, schema_type, access)?
384                    {
385                        return Ok(());
386                    }
387                    return Err(Error::InstanceUnassessed { node, kind });
388                }
389            }
390            if matches!(rule, Rule::Open) {
391                return Ok(());
392            }
393            if !name.namespace.is_empty() {
394                return Err(Error::Unexpected {
395                    node,
396                    attribute: current,
397                });
398            }
399            let selected = field(rule, name.local).ok_or(Error::Unexpected {
400                node,
401                attribute: current,
402            })?;
403            if matches!(selected, Field::Required(_)) {
404                required = true;
405            }
406            if let Field::Typed(datatype) | Field::Required(datatype) = selected {
407                scalar::check(access, node, LexicalSource::Attribute(name), datatype)
408                    .map_err(Error::Scalar)?;
409            }
410            Ok(())
411        })
412    });
413    match visited {
414        Ok(()) => (),
415        Err(AccessError::Xml(VisitError::Processing(cause))) => {
416            return Err(Error::Access(AccessError::Xml(cause)));
417        }
418        Err(AccessError::Xml(VisitError::Visitor(cause))) => return Err(cause),
419        Err(AccessError::Changed { node }) => {
420            return Err(Error::Access(AccessError::Changed { node }));
421        }
422        Err(AccessError::Resource { node, cause }) => return Err(resource(node, cause)),
423    }
424    if !required {
425        let local = match rule {
426            Rule::Target => Some("Target"),
427            Rule::Include | Rule::RequiredUri => Some("URI"),
428            Rule::Format => Some("ObjectReference"),
429            Rule::Order => Some("Order"),
430            _ => None,
431        };
432        if let Some(local) = local {
433            return Err(Error::Missing { node, local });
434        }
435    }
436    Ok(())
437}
438
439fn same_declared_type<X: AttributeXml, W: Work>(
440    node: X::Id,
441    attribute: Name<'_>,
442    expected: Name<'static>,
443    access: &Access<'_, X, W>,
444) -> Result<bool, Error<X::Error, X::Id>> {
445    enum Reading {
446        Compared(bool),
447        Problem(TypeProblem),
448    }
449    // XSD1.0 Structures §3.3.4(4–5): QName resolution is not derivation
450    // or validation under an overriding type. Equality safely retains the
451    // already selected declaration; other types remain explicitly unassessed.
452    // https://www.w3.org/TR/2004/REC-xmlschema-1-20041028/#cvc-elt
453    let reading = access.query_fallible(node, |xml, snapshot, work| {
454        xml.with_qname(
455            snapshot,
456            node,
457            LexicalSource::Attribute(attribute),
458            work,
459            |read| {
460                let value = match read.fact {
461                    QNameFact::Value(value) => value,
462                    QNameFact::Unsupported => return Ok(Reading::Compared(false)),
463                    QNameFact::Absent => return Ok(Reading::Problem(TypeProblem::Absent)),
464                    QNameFact::ComplexContent => {
465                        return Ok(Reading::Problem(TypeProblem::ComplexContent));
466                    }
467                    QNameFact::Malformed => return Ok(Reading::Problem(TypeProblem::Malformed)),
468                    QNameFact::UnboundPrefix(_) => {
469                        return Ok(Reading::Problem(TypeProblem::UnboundPrefix));
470                    }
471                };
472                let Some(namespace) = value.namespace else {
473                    return Ok(Reading::Compared(false));
474                };
475                if !access.equal(namespace, expected.namespace)? {
476                    return Ok(Reading::Compared(false));
477                }
478                access
479                    .equal(value.local_name, expected.local)
480                    .map(Reading::Compared)
481            },
482        )
483    });
484    match reading {
485        Ok(Reading::Compared(value)) => Ok(value),
486        Ok(Reading::Problem(problem)) => Err(Error::Type { node, problem }),
487        Err(AccessError::Xml(VisitError::Processing(cause))) => {
488            Err(Error::Access(AccessError::Xml(cause)))
489        }
490        Err(AccessError::Xml(VisitError::Visitor(cause))) => Err(resource(node, cause)),
491        Err(AccessError::Changed { node }) => Err(Error::Access(AccessError::Changed { node })),
492        Err(AccessError::Resource { node, cause }) => Err(resource(node, cause)),
493    }
494}
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499
500    #[test]
501    fn certified_role_attribute_type_depends_on_its_original_parent_declaration() {
502        // Annex C gives legacy CertifiedRole EncapsulatedPKIDataType, while
503        // CertifiedRolesV2's identically named child is an attribute-free
504        // choice wrapper. Local name alone must not grant Id/Encoding there.
505        let parent = declaration::declaration(
506            Name {
507                namespace: XADES,
508                local: "CertifiedRolesV2",
509            },
510            Context::Ordinary,
511        )
512        .unwrap();
513        let wrapper = declaration::declaration(
514            Name {
515                namespace: XADES,
516                local: "CertifiedRole",
517            },
518            parent.context,
519        )
520        .unwrap();
521        assert!(matches!(wrapper.rule, Rule::None));
522        assert!(wrapper.descend);
523        let legacy = declaration::declaration(
524            Name {
525                namespace: XADES,
526                local: "CertifiedRole",
527            },
528            Context::Ordinary,
529        )
530        .unwrap();
531        assert!(matches!(legacy.rule, Rule::Encoding));
532        assert!(!legacy.descend);
533    }
534}