Skip to main content

xml_sec/xmldsig/verify/
session.rs

1//! Continuations over the original verified document and operation state.
2
3use super::*;
4use structured_asn1::memory::{MemoryError, OwnedMap, OwnedText, OwnedVec, Work};
5
6mod hash_uses;
7pub use hash_uses::*;
8mod key_receipt;
9pub use key_receipt::*;
10mod lookup;
11pub use lookup::*;
12
13type RetainedOperation<'scope, 'inputs, W> = CapturedOperation<
14    &'scope crate::policy::VerificationPolicy,
15    &'scope VerificationOperationBudgets<'inputs>,
16    ScopedCapture<'scope, W>,
17>;
18
19/// A verified request's scoped continuation. No document, policy, resolver or
20/// budget is reconstructed when selecting a signature or replaying a reference.
21pub struct VerifiedSession<'view, 'scope, 'inputs, W: Work> {
22    view: DocumentView<'view>,
23    context: &'scope VerifyContext<'inputs>,
24    budgets: &'scope VerificationOperationBudgets<'inputs>,
25    evidence: VerifyEvidence<OwnedVec<SignatureEvidence, W>>,
26    operations: OwnedVec<RetainedOperation<'scope, 'inputs, W>, W>,
27    resolver: UriReferenceResolver<'view>,
28    work: &'scope W,
29    replay_active: Cell<bool>,
30}
31
32/// Verify once, then continue within the same document generation, selected
33/// provider, immutable policy, captured inputs and remaining operation budgets.
34/// The callback cannot return document- or operation-borrowing handles.
35pub fn with_verified_request<'inputs, W: Work, R>(
36    document: &XmlDocument,
37    context: &VerifyContext<'inputs>,
38    request: &VerificationRequest<'inputs>,
39    work: &W,
40    continuation: impl for<'view, 'scope> FnOnce(
41        VerifiedSession<'view, 'scope, 'inputs, W>,
42    ) -> Result<R, DsigError>,
43) -> Result<R, DsigError> {
44    with_verified_document(
45        document,
46        context,
47        request,
48        ScopedRequestStorage(work),
49        |view, context, budgets, evidence, operations, resolver| {
50            continuation(VerifiedSession {
51                view,
52                context,
53                budgets,
54                evidence,
55                operations,
56                resolver,
57                work,
58                replay_active: Cell::new(false),
59            })
60        },
61    )
62}
63
64/// The exact authenticated parent of a reference instruction.
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum ReferenceParent {
67    /// Direct child of this selected Signature's SignedInfo.
68    SignedInfo(NodeIdentity),
69    /// Direct child of an authenticated original Manifest.
70    Manifest(NodeIdentity),
71}
72
73/// Original reference metadata. Authentication of the instruction is separate
74/// from the historical target digest result and conservative content coverage.
75#[derive(Debug)]
76pub struct AuthenticatedReference<'a> {
77    /// Original document/generation-bound Reference identity.
78    pub identity: NodeIdentity,
79    /// Exact original SignedInfo or Manifest parent.
80    pub parent: ReferenceParent,
81    /// Original URI attribute, preserving omitted versus empty.
82    pub uri: Option<&'a str>,
83    /// Original Type attribute; not inferred from target shape.
84    pub reference_type: Option<&'a str>,
85    /// Historical result, absent when target execution was not completed.
86    pub result: Option<&'a ReferenceResult>,
87    /// Observed resource provenance, not a guess from URI spelling or a missing target.
88    pub location: ReferenceLocation,
89}
90
91/// The original admitted resource route. `Unchecked` does not mean detached.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum ReferenceLocation {
94    /// Original same-document target observed by reference verification.
95    SameDocument(NodeIdentity),
96    /// Admitted external lookup, with fingerprint only when bytes were supplied.
97    Detached(Option<[u8; 32]>),
98    /// No completed resource provenance was recorded.
99    Unchecked,
100}
101
102/// Instruction discovery completeness, not a claim that every target is valid.
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104pub enum CatalogueCompleteness {
105    /// All authenticated original instructions were visited.
106    Complete,
107    /// Original authenticated instructions were not processed by verification.
108    Unchecked(NodeIdentity),
109    /// An authenticated Manifest has not completed discovery/processing.
110    Incomplete(NodeIdentity),
111}
112
113/// Visitor refusal remains distinct from XML/policy/resource processing errors.
114#[derive(Debug)]
115pub enum CatalogueError<E> {
116    /// Original processing failure.
117    Processing(DsigError),
118    /// Original caller cause, without string conversion or boxing.
119    Visitor(E),
120}
121
122fn flatten_catalogue<T, E>(
123    result: Result<T, CatalogueError<CatalogueError<E>>>,
124) -> Result<T, CatalogueError<E>> {
125    match result {
126        Ok(value) => Ok(value),
127        Err(CatalogueError::Processing(error)) => Err(CatalogueError::Processing(error)),
128        Err(CatalogueError::Visitor(error)) => Err(error),
129    }
130}
131
132/// Where the original property supplies its URI. Attribute lookup uses an
133/// expanded name; simple content joins only direct text/CDATA in source order.
134#[derive(Clone, Copy, Debug)]
135pub enum LexicalSource<'a> {
136    /// Exact original attribute; absence is not an empty URI.
137    Attribute {
138        /// Namespace URI, or no namespace (default namespaces do not apply to attributes).
139        namespace: Option<&'a str>,
140        /// Local name within that namespace.
141        local_name: &'a str,
142    },
143    /// Simple content; nested elements are malformed, comments/PIs contribute no text.
144    SimpleContent,
145}
146
147/// Semantic resolution facts, independent of terminal processing failures.
148#[derive(Debug)]
149pub enum ReferenceResolution<'a> {
150    /// The selected attribute is absent.
151    Absent,
152    /// Simple content contains an element child.
153    Malformed,
154    /// The original URI is outside the implemented same-document grammar.
155    /// Policy and resource refusal are processing errors, not this disposition.
156    Unsupported,
157    /// The admitted URI selects caller-owned external data, not an original XML Reference.
158    Detached,
159    /// The ID is missing or ambiguous under the operation's ID registrations.
160    Unresolved,
161    /// Original target is not a ds:Reference.
162    NotReference(NodeIdentity),
163    /// Original target instruction is not authenticated by this selected Signature.
164    Unauthenticated(NodeIdentity),
165    /// Original authenticated instruction, including exact parent and historical status.
166    Reference(AuthenticatedReference<'a>),
167}
168
169enum LexicalValue<'a, W: Work> {
170    Borrowed(&'a str),
171    Joined(OwnedText<W>),
172}
173impl<W: Work> LexicalValue<'_, W> {
174    fn as_str(&self) -> &str {
175        match self {
176            Self::Borrowed(value) => value,
177            // Appending original semantic str fragments preserves UTF-8. This
178            // view must not perform another unadmitted full-prefix validation.
179            Self::Joined(value) => value.as_str(),
180        }
181    }
182}
183
184/// A selected signature, bound to this request rather than an aggregate boolean.
185pub struct SelectedSignature<'session, 'view, 'scope, 'inputs, W: Work, Q: Work = W> {
186    session: &'session VerifiedSession<'view, 'scope, 'inputs, W>,
187    index: usize,
188    work: &'session Q,
189    results: OwnedMap<NodeIdentity, (ReferenceSet, usize), Q>,
190    replays: RefCell<OwnedVec<ReplayRecord, Q>>,
191}
192
193impl<'view, 'scope, 'inputs, W: Work> VerifiedSession<'view, 'scope, 'inputs, W> {
194    /// Original immutable view; identities never cross a second parse/projection.
195    pub fn view(&self) -> DocumentView<'view> {
196        self.view
197    }
198    /// Results of the original verification. Inspect errors and target status
199    /// independently of selected instruction authentication.
200    pub fn evidence(&self) -> &VerifyEvidence<OwnedVec<SignatureEvidence, W>> {
201        &self.evidence
202    }
203
204    /// Transfer independently owned result facts with their retained account.
205    /// This transfers no document or operation borrow and no replay capability.
206    pub fn into_evidence(self) -> VerifyEvidence<OwnedVec<SignatureEvidence, W>> {
207        self.evidence
208    }
209
210    /// Select only a Signature whose original cryptographic decision succeeded.
211    /// A failed child Manifest digest does not erase that decision.
212    pub fn select_signature(
213        &self,
214        identity: NodeIdentity,
215    ) -> Result<SelectedSignature<'_, 'view, 'scope, 'inputs, W>, DsigError> {
216        self.select_signature_in(identity, self.work)
217    }
218
219    /// Continue an original authenticated signature with a separate current
220    /// execution. Query scratch cannot escape the callback; the document,
221    /// provider, policy, authentication and retained storage remain original.
222    /// Selection, inspection, resolution and transform replay spend `work`;
223    /// the original compiled policy's cumulative allowances are not reset.
224    /// Use `select_signature` instead to retain producer-funded execution.
225    pub fn with_query<Q: Work, R, E>(
226        &self,
227        identity: NodeIdentity,
228        work: &Q,
229        visitor: impl for<'query> FnOnce(
230            SelectedSignature<'query, 'view, 'scope, 'inputs, W, Q>,
231        ) -> Result<R, E>,
232    ) -> Result<R, CatalogueError<E>> {
233        let selected = self
234            .select_signature_in(identity, work)
235            .map_err(CatalogueError::Processing)?;
236        visitor(selected).map_err(CatalogueError::Visitor)
237    }
238
239    fn select_signature_in<'query, Q: Work>(
240        &'query self,
241        identity: NodeIdentity,
242        work: &'query Q,
243    ) -> Result<SelectedSignature<'query, 'view, 'scope, 'inputs, W, Q>, DsigError> {
244        let index = self.authenticated_signature_in(identity, work)?;
245        let entry = &self.evidence.signatures()[index];
246        let mut results = work.map();
247        if let Ok(result) = &entry.result {
248            for (set, entries) in [
249                (ReferenceSet::SignedInfo, &result.signed_info_references),
250                (ReferenceSet::Manifest, &result.manifest_references),
251            ] {
252                for (ordinal, reference) in entries.iter().enumerate() {
253                    work.charge_work(1)?;
254                    if let Some(node) = reference.reference_identity
255                        && results.try_insert(node, (set, ordinal))?.is_some()
256                    {
257                        return Err(DsigError::InvalidRequest {
258                            reason: "ambiguous original Reference evidence",
259                        });
260                    }
261                }
262            }
263        }
264        Ok(SelectedSignature {
265            session: self,
266            index,
267            work,
268            results,
269            replays: RefCell::new(work.vector()),
270        })
271    }
272
273    // Key-only queries reuse authentication without allocating a Reference
274    // index or charging the completed verification's producer-local execution.
275    fn authenticated_signature_in<Q: Work>(
276        &self,
277        identity: NodeIdentity,
278        work: &Q,
279    ) -> Result<usize, DsigError> {
280        work.charge_work(1)?;
281        self.view.resolve_node(identity)?;
282        for (index, entry) in self.evidence.signatures().iter().enumerate() {
283            work.charge_work(1)?;
284            if entry.identity != identity {
285                continue;
286            }
287            let retained = &self.operations[index];
288            if let Some(error) = retained.capture.failure.get() {
289                return Err(error.into());
290            }
291            let operation = &retained.operation;
292            operation.validate_document_view(self.view)?;
293            work.charge_work(
294                u64::try_from(operation.decision_count()).map_err(|_| MemoryError::WorkExceeded)?,
295            )?;
296            if operation.accepted_signature().is_none() {
297                return Err(DsigError::InvalidRequest {
298                    reason: "selected Signature is not authenticated",
299                });
300            }
301            return Ok(index);
302        }
303        Err(DsigError::InvalidRequest {
304            reason: "Signature is not part of this request",
305        })
306    }
307
308    fn signature_method_in<Q: Work>(
309        &self,
310        index: usize,
311        work: &Q,
312    ) -> Result<SelectedSignatureMethod, DsigError> {
313        work.charge_work(1)?;
314        let retained = &self.operations[index];
315        retained.operation.validate_document_view(self.view)?;
316        if let Some(error) = retained.capture.failure.get() {
317            return Err(error.into());
318        }
319        retained
320            .capture
321            .method
322            .get()
323            .ok_or(DsigError::InvalidRequest {
324                reason: "selected Signature has no original method receipt",
325            })
326    }
327}
328
329impl<'session, 'view, 'scope, 'inputs, W: Work, Q: Work>
330    SelectedSignature<'session, 'view, 'scope, 'inputs, W, Q>
331{
332    fn operation(
333        &self,
334    ) -> &OperationExecutionContext<
335        &'scope crate::policy::VerificationPolicy,
336        &'scope VerificationOperationBudgets<'inputs>,
337    > {
338        &self.session.operations[self.index].operation
339    }
340    fn signature(&self) -> Result<Node<'_, '_>, DsigError> {
341        Ok(self
342            .session
343            .view
344            .resolve_node(self.session.evidence.signatures()[self.index].identity)?)
345    }
346    fn authenticates_element(&self, node: Node<'_, '_>) -> Result<bool, DsigError> {
347        let signature = self.signature()?;
348        for ancestor in node.ancestors() {
349            self.work.charge_work(1)?;
350            if ancestor.has_tag_name((XMLDSIG_NS, "SignedInfo"))
351                && ancestor.parent() == Some(signature)
352            {
353                return Ok(true);
354            }
355            if self
356                .operation()
357                .is_authenticated(self.session.view.node_identity(ancestor))
358            {
359                return Ok(true);
360            }
361        }
362        Ok(false)
363    }
364
365    /// Resolve from the authenticated original property's lexical source, using
366    /// this request's URI grammar, ID registrations, policy and XML Base budget.
367    /// No network fetch, independent ID scan or fabricated Reference is involved.
368    pub fn resolve_reference(
369        &self,
370        property: NodeIdentity,
371        source: LexicalSource<'_>,
372    ) -> Result<ReferenceResolution<'_>, DsigError> {
373        self.work.charge_work(1)?;
374        self.operation().validate_document_view(self.session.view)?;
375        let node = self.session.view.resolve_node(property)?;
376        if !node.is_element() {
377            return Err(DsigError::InvalidRequest {
378                reason: "reference property must be an element",
379            });
380        }
381        if !self.authenticates_element(node)? {
382            return Ok(ReferenceResolution::Unauthenticated(property));
383        }
384        let value = match source {
385            LexicalSource::Attribute {
386                namespace,
387                local_name,
388            } => {
389                let mut value = None;
390                for attribute in node.attributes() {
391                    let bytes = attribute
392                        .name()
393                        .len()
394                        .checked_add(attribute.namespace().map_or(0, str::len))
395                        .and_then(|n| n.checked_add(1))
396                        .ok_or(MemoryError::WorkExceeded)?;
397                    self.work.charge_work(
398                        u64::try_from(bytes).map_err(|_| MemoryError::WorkExceeded)?,
399                    )?;
400                    if attribute.name() == local_name && attribute.namespace() == namespace {
401                        value = Some(attribute.value());
402                        break;
403                    }
404                }
405                let Some(value) = value else {
406                    return Ok(ReferenceResolution::Absent);
407                };
408                LexicalValue::Borrowed(value)
409            }
410            LexicalSource::SimpleContent => {
411                let mut value = LexicalValue::Borrowed("");
412                for child in node.children() {
413                    self.work.charge_work(1)?;
414                    if child.is_element() {
415                        return Ok(ReferenceResolution::Malformed);
416                    }
417                    let Some(text) = child.text().filter(|_| child.is_text()) else {
418                        continue;
419                    };
420                    match &mut value {
421                        LexicalValue::Borrowed(first) if first.is_empty() => *first = text,
422                        LexicalValue::Borrowed(first) => {
423                            let mut joined = OwnedText::try_from_str(first, self.work)?;
424                            joined.try_push_str(text)?;
425                            value = LexicalValue::Joined(joined);
426                        }
427                        LexicalValue::Joined(joined) => joined.try_push_str(text)?,
428                    }
429                }
430                value
431            }
432        };
433        let uri = value.as_str();
434        self.work
435            .charge_work(u64::try_from(uri.len()).map_err(|_| MemoryError::WorkExceeded)?)?;
436        if !self.session.context.policy.uris.references.allows(uri) {
437            return Err(crate::policy::PolicyViolation::Uri {
438                operation: "extension reference resolution",
439                reason: "reference URI class is not permitted",
440            }
441            .into());
442        }
443        if !uri.is_empty() && !uri.starts_with('#') {
444            crate::c14n::xml_base::resolve_uri_from_node_in(
445                node,
446                uri,
447                None,
448                self.session.budgets.transforms.xml_base_resolution(),
449                &crate::c14n::scratch::Accounted(self.work),
450            )
451            .map_err(super::super::uri::map_xml_base_execution_error)
452            .map_err(|error| {
453                DsigError::Reference(ReferenceProcessingError::UriDereference(error))
454            })?;
455            return Ok(ReferenceResolution::Detached);
456        }
457        let target = self.session.resolver.node_for_same_document_reference_in(
458            uri,
459            Some(self.session.budgets.transforms.node_set_materialization()),
460            &crate::c14n::scratch::Accounted(self.work),
461        );
462        let target = match target {
463            Ok(target) => target,
464            Err(
465                TransformError::UnsupportedUri(_)
466                | TransformError::AccountedUnsupportedUri(_)
467                | TransformError::InvalidFragment,
468            ) => return Ok(ReferenceResolution::Unsupported),
469            Err(error) => {
470                return Err(DsigError::Reference(
471                    ReferenceProcessingError::UriDereference(error),
472                ));
473            }
474        };
475        let Some(target) = target else {
476            return Ok(ReferenceResolution::Unresolved);
477        };
478        let identity = self.session.view.node_identity(target);
479        if !target.has_tag_name((XMLDSIG_NS, "Reference")) {
480            return Ok(ReferenceResolution::NotReference(identity));
481        }
482        match self.descriptor(identity)? {
483            Some(reference) => Ok(ReferenceResolution::Reference(reference)),
484            None => Ok(ReferenceResolution::Unauthenticated(identity)),
485        }
486    }
487    fn parent(&self, reference: Node<'_, '_>) -> Result<Option<ReferenceParent>, DsigError> {
488        if !reference.has_tag_name((XMLDSIG_NS, "Reference")) {
489            return Ok(None);
490        }
491        let Some(parent) = reference.parent() else {
492            return Ok(None);
493        };
494        let signature = self.signature()?;
495        let view = self.session.view;
496        if parent.has_tag_name((XMLDSIG_NS, "SignedInfo")) && parent.parent() == Some(signature) {
497            return Ok(Some(ReferenceParent::SignedInfo(
498                view.node_identity(parent),
499            )));
500        }
501        if !parent.has_tag_name((XMLDSIG_NS, "Manifest")) {
502            return Ok(None);
503        }
504        for ancestor in parent.ancestors() {
505            self.work.charge_work(1)?;
506            if self
507                .operation()
508                .is_authenticated(view.node_identity(ancestor))
509            {
510                return Ok(Some(ReferenceParent::Manifest(view.node_identity(parent))));
511            }
512        }
513        Ok(None)
514    }
515    fn descriptor(
516        &self,
517        reference: NodeIdentity,
518    ) -> Result<Option<AuthenticatedReference<'_>>, DsigError> {
519        self.work.charge_work(1)?;
520        self.operation().validate_document_view(self.session.view)?;
521        let node = self.session.view.resolve_node(reference)?;
522        let Some(parent) = self.parent(node)? else {
523            return Ok(None);
524        };
525        self.descriptor_in_parent(node, parent).map(Some)
526    }
527
528    fn descriptor_in_parent<'a>(
529        &'a self,
530        node: Node<'a, 'a>,
531        parent: ReferenceParent,
532    ) -> Result<AuthenticatedReference<'a>, DsigError> {
533        let reference = self.session.view.node_identity(node);
534        let result = match self.results.get(&reference, self.work)? {
535            Some((set, index)) => self.session.evidence.signatures()[self.index]
536                .result
537                .as_ref()
538                .ok()
539                .map(|result| match set {
540                    ReferenceSet::SignedInfo => &result.signed_info_references[*index],
541                    ReferenceSet::Manifest => &result.manifest_references[*index],
542                }),
543            None => None,
544        };
545        // Admit both URI/Type attribute lookups before performing them. Their
546        // fixed names bound each comparison; values themselves remain borrowed.
547        for _ in node.attributes() {
548            self.work.charge_work(9)?;
549        }
550        Ok(AuthenticatedReference {
551            identity: reference,
552            parent,
553            uri: node.attribute("URI"),
554            reference_type: node.attribute("Type"),
555            result,
556            location: if let Some(target) = result.and_then(|result| result.target_identity) {
557                ReferenceLocation::SameDocument(target)
558            } else {
559                match self
560                    .session
561                    .budgets
562                    .resolved_reference_resources
563                    .borrow()
564                    .get(&node.id())
565                {
566                    Some(OperationResourceIdentity::External { fingerprint, .. }) => {
567                        ReferenceLocation::Detached(Some(*fingerprint))
568                    }
569                    Some(OperationResourceIdentity::MissingExternal) => {
570                        ReferenceLocation::Detached(None)
571                    }
572                    _ => ReferenceLocation::Unchecked,
573                }
574            },
575        })
576    }
577
578    /// Paid, single-pass original catalogue. No repeated ordinal rescanning;
579    /// callbacks borrow descriptors and may retain only copied stable identities.
580    pub fn visit_authenticated_references<E>(
581        &self,
582        mut visitor: impl for<'a> FnMut(AuthenticatedReference<'a>) -> Result<(), E>,
583    ) -> Result<CatalogueCompleteness, CatalogueError<E>> {
584        flatten_catalogue(self.visit_reference_instructions(|node, parent, _| {
585            let reference = self
586                .descriptor_in_parent(node, parent)
587                .map_err(CatalogueError::Processing)?;
588            visitor(reference).map_err(CatalogueError::Visitor)
589        }))
590    }
591
592    fn visit_reference_instructions<E>(
593        &self,
594        mut visitor: impl for<'a> FnMut(
595            Node<'a, 'a>,
596            ReferenceParent,
597            Option<Node<'a, 'a>>,
598        ) -> Result<(), E>,
599    ) -> Result<CatalogueCompleteness, CatalogueError<E>> {
600        let mut completeness = CatalogueCompleteness::Complete;
601        let mut references = 0;
602        let maximum = self.session.context.policy.resources.max_references;
603        self.operation()
604            .validate_document_view(self.session.view)
605            .map_err(|error| CatalogueError::Processing(error.into()))?;
606        for node in self.session.view.document().descendants() {
607            self.work
608                .charge_work(1)
609                .map_err(|error| CatalogueError::Processing(error.into()))?;
610            if node.has_tag_name((XMLDSIG_NS, "Manifest"))
611                && self
612                    .authenticates_element(node)
613                    .map_err(CatalogueError::Processing)?
614            {
615                let mut count = 0;
616                let mut malformed = false;
617                for child in node.children() {
618                    self.work
619                        .charge_work(1)
620                        .map_err(|error| CatalogueError::Processing(error.into()))?;
621                    if child.has_tag_name((XMLDSIG_NS, "Reference")) {
622                        count += 1;
623                    } else if child.is_element() {
624                        malformed = true;
625                    } else if child.is_text() {
626                        let text = child.text().unwrap_or_default();
627                        self.work
628                            .charge_work(text.len() as u64)
629                            .map_err(|error| CatalogueError::Processing(error.into()))?;
630                        if !is_xml_whitespace_only(text) {
631                            malformed = true;
632                        }
633                    }
634                }
635                if (malformed || count == 0)
636                    && !matches!(completeness, CatalogueCompleteness::Incomplete(_))
637                {
638                    completeness =
639                        CatalogueCompleteness::Incomplete(self.session.view.node_identity(node));
640                }
641            }
642            if !node.has_tag_name((XMLDSIG_NS, "Reference")) {
643                continue;
644            }
645            let Some(parent) = self.parent(node).map_err(CatalogueError::Processing)? else {
646                continue;
647            };
648            let identity = self.session.view.node_identity(node);
649            if references == maximum {
650                return Err(CatalogueError::Processing(
651                    crate::policy::PolicyViolation::ResourceLimit {
652                        resource: crate::policy::resource_name::SIGNATURE_REFERENCES,
653                        maximum,
654                        actual: references + 1,
655                    }
656                    .into(),
657                ));
658            }
659            references += 1;
660            self.work
661                .charge_work(128)
662                .map_err(|error| CatalogueError::Processing(error.into()))?;
663            for child in node.children() {
664                let text = child.text().filter(|_| child.is_text()).map_or(0, str::len);
665                let cost = u64::try_from(text)
666                    .ok()
667                    .and_then(|length| length.checked_add(128))
668                    .ok_or_else(|| CatalogueError::Processing(MemoryError::WorkExceeded.into()))?;
669                self.work
670                    .charge_work(cost)
671                    .map_err(|error| CatalogueError::Processing(error.into()))?;
672            }
673            let method = super::super::parse::inspect_reference_layout(node)
674                .ok()
675                .map(|(_, method, _)| method);
676            if method.is_none() {
677                if !matches!(completeness, CatalogueCompleteness::Incomplete(_)) {
678                    completeness = CatalogueCompleteness::Incomplete(identity);
679                }
680            } else if let ReferenceParent::Manifest(parent) = parent
681                && matches!(completeness, CatalogueCompleteness::Complete)
682                && self.session.context.policy.manifest_processing
683                    == crate::policy::ManifestProcessing::Ignore
684            {
685                completeness = CatalogueCompleteness::Unchecked(parent);
686            }
687            visitor(node, parent, method).map_err(CatalogueError::Visitor)?;
688        }
689        Ok(completeness)
690    }
691}
692
693/// Final node-set coercion. Explicit final C14N transforms already produce
694/// octets and therefore take precedence over either implicit mode.
695#[derive(Clone, Copy, Debug, PartialEq, Eq)]
696pub enum ReplayMode {
697    /// XMLDSig 1.1 §4.3.3.2 implicit C14N 1.0.
698    ReferenceOutput,
699    /// Original timestamp CanonicalizationMethod node, including parameters.
700    Timestamp(NodeIdentity),
701}
702
703/// Missing explicitly supplied external bytes are diagnostic, not a successful
704/// empty output. Policy, malformed XML and processing failures remain errors.
705#[derive(Clone, Copy, Debug, PartialEq, Eq)]
706pub enum ReplayOutcome {
707    /// The receiver obtained the entire output.
708    Complete,
709    /// The request did not supply this admitted external resource.
710    Unavailable(ExternalResourceUnavailable),
711}
712
713/// The admitted external lookup was absent from the captured caller map.
714#[derive(Clone, Copy, Debug, PartialEq, Eq)]
715pub struct ExternalResourceUnavailable {
716    /// Original authenticated Reference whose lookup was absent.
717    pub reference: NodeIdentity,
718}
719
720/// Typed sticky receiver failure, independent of XML processing failure.
721#[derive(Debug)]
722pub enum ReplayError<E> {
723    /// XML, policy, provenance or resource failure.
724    Processing(DsigError),
725    /// Original receiver error.
726    Receiver(E),
727}
728impl<E> From<TransformError> for ReplayError<E> {
729    fn from(error: TransformError) -> Self {
730        Self::Processing(match error {
731            TransformError::C14n(crate::c14n::C14nError::SharedExecution(error)) => error.into(),
732            error => ReferenceProcessingError::Transform(error).into(),
733        })
734    }
735}
736impl<E> From<OperationPlanError> for ReplayError<E> {
737    fn from(error: OperationPlanError) -> Self {
738        Self::Processing(error.into())
739    }
740}
741
742struct ReplayWriter<'a, W: Work, E, F> {
743    work: &'a W,
744    budget: &'a TransformExecutionBudget,
745    canonicalizing: bool,
746    receiver: &'a mut F,
747    failure: Option<ReplayError<E>>,
748}
749
750struct ReplayTerminal<'a, W: Work, E, F> {
751    work: &'a W,
752    receiver: &'a mut F,
753    timestamp: Option<(crate::c14n::C14nAlgorithm, Option<&'a str>)>,
754    error: std::marker::PhantomData<fn() -> E>,
755}
756
757impl<'a, W: Work, E, F: FnMut(&[u8]) -> Result<(), E>>
758    super::super::transforms::TransformTerminal<crate::c14n::scratch::Accounted<'a, W>>
759    for ReplayTerminal<'a, W, E, F>
760{
761    type Output = ();
762    type Error = ReplayError<E>;
763
764    fn finish<D: crate::xml::dom::storage::Storage>(
765        self,
766        data: super::super::transforms::ExecutionData<
767            '_,
768            crate::c14n::scratch::Accounted<'a, W>,
769            D,
770        >,
771        explicit: Option<(&crate::c14n::C14nAlgorithm, Option<&str>)>,
772        budget: &TransformExecutionBudget,
773    ) -> Result<(), ReplayError<E>> {
774        let mut output = ReplayWriter {
775            work: self.work,
776            budget,
777            canonicalizing: data.nodes().is_some(),
778            receiver: self.receiver,
779            failure: None,
780        };
781        let result = if let Some(bytes) = data.binary() {
782            use std::io::Write;
783            output.write_all(bytes).map_err(crate::c14n::C14nError::Io)
784        } else {
785            let nodes = data.nodes().expect("execution has nodes or bytes");
786            let implicit = crate::c14n::C14nAlgorithm::from_uri(DEFAULT_IMPLICIT_C14N_URI)
787                .expect("constant C14N URI");
788            let algorithm = explicit
789                .map(|(algorithm, _)| algorithm)
790                .or(self.timestamp.as_ref().map(|(algorithm, _)| algorithm))
791                .unwrap_or(&implicit);
792            let prefixes = if explicit.is_none() {
793                self.timestamp.as_ref().and_then(|(_, prefixes)| *prefixes)
794            } else {
795                explicit.and_then(|(_, prefixes)| prefixes)
796            };
797            crate::c14n::canonicalize_visible_to_writer_accounted(
798                nodes.document(),
799                Some(nodes),
800                algorithm,
801                super::super::transforms::replay_output_limit(budget),
802                budget.xml_base_resolution(),
803                &mut output,
804                self.work,
805                prefixes,
806            )
807        };
808        if let Some(error) = output.failure {
809            return Err(error);
810        }
811        result.map_err(|error| match error {
812            crate::c14n::C14nError::SharedExecution(error) => ReplayError::Processing(error.into()),
813            error => super::super::transforms::replay_c14n_error(error, budget).into(),
814        })
815    }
816}
817
818#[derive(Clone, Copy, Debug, PartialEq, Eq)]
819enum ReplayState {
820    Pending,
821    Complete,
822    Unavailable,
823    Rejected,
824}
825struct ReplayRecord {
826    signature: NodeIdentity,
827    reference: NodeIdentity,
828    state: ReplayState,
829}
830
831struct AdmittedReplay<'a, W: Work> {
832    records: &'a RefCell<OwnedVec<ReplayRecord, W>>,
833    active: &'a Cell<bool>,
834    index: usize,
835}
836impl<W: Work> AdmittedReplay<'_, W> {
837    fn finish<E>(&self, result: &Result<ReplayOutcome, ReplayError<E>>) {
838        self.records.borrow_mut()[self.index].state = match result {
839            Ok(ReplayOutcome::Complete) => ReplayState::Complete,
840            Ok(ReplayOutcome::Unavailable(_)) => ReplayState::Unavailable,
841            Err(_) => ReplayState::Rejected,
842        };
843    }
844}
845impl<W: Work> Drop for AdmittedReplay<'_, W> {
846    fn drop(&mut self) {
847        let mut records = self.records.borrow_mut();
848        if records[self.index].state == ReplayState::Pending {
849            records[self.index].state = ReplayState::Rejected;
850        }
851        self.active.set(false);
852    }
853}
854impl<W: Work, E, F: FnMut(&[u8]) -> Result<(), E>> std::io::Write for ReplayWriter<'_, W, E, F> {
855    fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
856        if self.failure.is_some() {
857            return Err(std::io::ErrorKind::Other.into());
858        }
859        let result = (|| {
860            let units = u64::try_from(bytes.len())
861                .map_err(|_| ReplayError::Processing(MemoryError::WorkExceeded.into()))?;
862            self.work
863                .charge_work(units)
864                .map_err(|error| ReplayError::Processing(error.into()))?;
865            if self.canonicalizing {
866                super::super::transforms::charge_replay_output(self.budget, bytes.len())?;
867            }
868            (self.receiver)(bytes).map_err(ReplayError::Receiver)
869        })();
870        match result {
871            Ok(()) => Ok(bytes.len()),
872            Err(error) => {
873                self.failure = Some(error);
874                Err(std::io::ErrorKind::Other.into())
875            }
876        }
877    }
878    fn flush(&mut self) -> std::io::Result<()> {
879        Ok(())
880    }
881}
882
883impl<W: Work, Q: Work> SelectedSignature<'_, '_, '_, '_, W, Q> {
884    /// Re-execute an authenticated instruction, not its historical digest test.
885    /// EN 319 132-1 V1.3.1 §5.5.3 steps 3/6 and Note 2 deliberately permit
886    /// renewal without recomputing the original digest algorithm.
887    /// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
888    pub fn replay_reference<E>(
889        &self,
890        reference: NodeIdentity,
891        mode: ReplayMode,
892        receiver: &mut impl FnMut(&[u8]) -> Result<(), E>,
893    ) -> Result<ReplayOutcome, ReplayError<E>> {
894        self.work
895            .charge_work(
896                u64::try_from(self.operation().decision_count())
897                    .map_err(|_| ReplayError::Processing(MemoryError::WorkExceeded.into()))?,
898            )
899            .map_err(|error| ReplayError::Processing(error.into()))?;
900        self.operation()
901            .run_reference_replay(self.session.view, reference, || {
902                self.replay_admitted(reference, mode, receiver)
903            })
904    }
905
906    fn replay_admitted<E>(
907        &self,
908        reference: NodeIdentity,
909        mode: ReplayMode,
910        receiver: &mut impl FnMut(&[u8]) -> Result<(), E>,
911    ) -> Result<ReplayOutcome, ReplayError<E>> {
912        let processing = ReplayError::Processing;
913        let descriptor = self
914            .descriptor(reference)
915            .map_err(processing)?
916            .ok_or_else(|| {
917                processing(DsigError::InvalidRequest {
918                    reason: "Reference instruction is not authenticated by selected Signature",
919                })
920            })?;
921        if matches!(descriptor.parent, ReferenceParent::Manifest(_))
922            && self.session.context.policy.manifest_processing
923                == crate::policy::ManifestProcessing::Ignore
924        {
925            return Err(processing(
926                crate::policy::PolicyViolation::ManifestProcessingDisabled.into(),
927            ));
928        }
929        if self.session.replay_active.get() {
930            return Err(processing(DsigError::InvalidRequest {
931                reason: "reentrant replay would reset the active transform chain",
932            }));
933        }
934        let index = {
935            let mut records = self.replays.borrow_mut();
936            let index = records.len();
937            records
938                .try_push(ReplayRecord {
939                    signature: self.session.evidence.signatures()[self.index].identity,
940                    reference,
941                    state: ReplayState::Pending,
942                })
943                .map_err(|error| processing(error.into()))?;
944            debug_assert_eq!(records[index].reference, reference);
945            debug_assert_eq!(
946                records[index].signature,
947                self.session.evidence.signatures()[self.index].identity
948            );
949            index
950        };
951        self.session.replay_active.set(true);
952        let admission = AdmittedReplay {
953            records: &self.replays,
954            active: &self.session.replay_active,
955            index,
956        };
957        let result = self.replay_instruction(descriptor, mode, receiver);
958        admission.finish(&result);
959        result
960    }
961
962    fn replay_instruction<E>(
963        &self,
964        descriptor: AuthenticatedReference<'_>,
965        mode: ReplayMode,
966        receiver: &mut impl FnMut(&[u8]) -> Result<(), E>,
967    ) -> Result<ReplayOutcome, ReplayError<E>> {
968        let processing = ReplayError::Processing;
969        let node = self
970            .session
971            .view
972            .resolve_node(descriptor.identity)
973            .map_err(|error| processing(error.into()))?;
974        let uri = descriptor.uri.ok_or_else(|| {
975            processing(DsigError::Reference(ReferenceProcessingError::MissingUri))
976        })?;
977        if !self.session.context.policy.uris.references.allows(uri) {
978            return Err(processing(
979                crate::policy::PolicyViolation::Uri {
980                    operation: "reference replay",
981                    reason: "reference URI class is not permitted",
982                }
983                .into(),
984            ));
985        }
986        let allowed = self
987            .session
988            .context
989            .policy
990            .transforms
991            .allowed_algorithms
992            .as_ref();
993        let maximum = self
994            .session
995            .context
996            .policy
997            .resources
998            .max_transforms_per_reference;
999        let mut transform_count = 0;
1000        self.work
1001            .charge_work(node.children().len() as u64)
1002            .map_err(|error| processing(error.into()))?;
1003        for transforms in node
1004            .children()
1005            .filter(|child| child.has_tag_name((XMLDSIG_NS, "Transforms")))
1006        {
1007            self.work
1008                .charge_work(transforms.children().len() as u64)
1009                .map_err(|error| processing(error.into()))?;
1010            for transform in transforms.children().filter(Node::is_element) {
1011                self.work
1012                    .charge_work(1)
1013                    .map_err(|error| processing(error.into()))?;
1014                transform_count += 1;
1015                if transform_count > maximum {
1016                    return Err(processing(
1017                        crate::policy::PolicyViolation::ResourceLimit {
1018                            resource: crate::policy::resource_name::REFERENCE_TRANSFORMS,
1019                            maximum,
1020                            actual: transform_count,
1021                        }
1022                        .into(),
1023                    ));
1024                }
1025                self.work
1026                    .charge_work(transform.attributes().len() as u64)
1027                    .map_err(|error| processing(error.into()))?;
1028                if let Some(algorithm) = transform.attribute("Algorithm") {
1029                    self.work
1030                        .charge_work(algorithm.len() as u64)
1031                        .map_err(|error| processing(error.into()))?;
1032                    enforce_current_transform_allowed(allowed, algorithm, self.work)
1033                        .map_err(processing)?;
1034                }
1035            }
1036        }
1037        let parse_failure = |error| match error {
1038            super::super::parse::ParseError::Transform(TransformError::C14n(
1039                crate::c14n::C14nError::SharedExecution(error),
1040            )) => processing(error.into()),
1041            error => processing(DsigError::ParseManifestReference(error)),
1042        };
1043        let transforms = super::super::parse::parse_replay_transforms(
1044            node,
1045            &mut self.session.budgets.xpath_parse.borrow_mut(),
1046            &crate::c14n::scratch::Accounted(self.work),
1047        )
1048        .map_err(parse_failure)?;
1049        if matches!(mode, ReplayMode::ReferenceOutput)
1050            && transform_chain_uses_implicit_c14n(
1051                classify_uri(uri) == UriClass::External,
1052                &transforms,
1053            )
1054        {
1055            enforce_transform_allowed(allowed, DEFAULT_IMPLICIT_C14N_URI).map_err(processing)?;
1056        }
1057        let timestamp = match mode {
1058            ReplayMode::ReferenceOutput => None,
1059            ReplayMode::Timestamp(method) => {
1060                let method = self
1061                    .session
1062                    .view
1063                    .resolve_node(method)
1064                    .map_err(|error| processing(error.into()))?;
1065                let uri = method.attribute("Algorithm").ok_or_else(|| {
1066                    processing(DsigError::InvalidStructure {
1067                        reason: "timestamp method has no Algorithm",
1068                    })
1069                })?;
1070                self.work
1071                    .charge_work(uri.len() as u64)
1072                    .map_err(|error| processing(error.into()))?;
1073                enforce_current_transform_allowed(allowed, uri, self.work).map_err(processing)?;
1074                let algorithm = super::super::parse::parse_replay_canonicalization_method(
1075                    method,
1076                    &crate::c14n::scratch::Accounted(self.work),
1077                )
1078                .map_err(parse_failure)?;
1079                Some(algorithm)
1080            }
1081        };
1082        let budget = &self.session.budgets.transforms;
1083        let Some(data) = self.session.resolver.replay_input_from_with_budget(
1084            uri,
1085            node,
1086            budget.node_set_materialization(),
1087            budget.xml_base_resolution(),
1088            &crate::c14n::scratch::Accounted(self.work),
1089        )?
1090        else {
1091            return Ok(ReplayOutcome::Unavailable(ExternalResourceUnavailable {
1092                reference: descriptor.identity,
1093            }));
1094        };
1095        let enveloped_signature =
1096            super::super::transforms::enveloped_signature_for_reference(node, &transforms, || {
1097                self.work
1098                    .charge_work(1)
1099                    .map_err(|error| processing(error.into()))
1100            })?;
1101        super::super::transforms::replay_reference_transforms(
1102            node,
1103            enveloped_signature,
1104            data,
1105            &transforms,
1106            super::super::transforms::ReplayExecution {
1107                options: self.session.context.transform_options(),
1108                budget,
1109                work: self.work,
1110            },
1111            ReplayTerminal {
1112                work: self.work,
1113                receiver,
1114                timestamp,
1115                error: std::marker::PhantomData,
1116            },
1117        )?;
1118        Ok(ReplayOutcome::Complete)
1119    }
1120}