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