Skip to main content

structured_cms/verify/
document.rs

1//! A CMS batch whose input is bound to one original `SignedData` container.
2
3use super::{Batch, CheckError, Checked, PassError};
4use crate::prepare::ResolvedSigner;
5use crate::provider::SignatureProvider;
6use crate::stream::Source;
7use crate::{Error, ErrorCode, SignedData};
8use structured_asn1::Reader;
9use structured_asn1::memory::{Execution, OwnedVec, Work};
10
11/// Explicit original content selection before provider initialization.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ContentMode {
14    /// Original eContent octets carried by this `SignedData`.
15    Carried,
16    /// An explicit caller source for a `SignedData` WITHOUT eContent.
17    Detached,
18}
19
20/// Existing verification driver plus the exact original source association.
21/// The selected root signers share one payload pass and one digest memo.
22pub struct DocumentBatch<'operation, 'a, P: SignatureProvider<'a>> {
23    message: &'a SignedData<'a>,
24    batch: Batch<'a, P>,
25    mode: ContentMode,
26    execution: Execution<'operation>,
27}
28
29impl<'operation, 'a, P: SignatureProvider<'a>> DocumentBatch<'operation, 'a, P> {
30    /// Checks container/signer provenance and explicit input mode BEFORE any
31    /// provider initialization, then delegates to the one existing batch core.
32    /// Detached source acquisition remains caller-owned; it is never a fallback
33    /// for a carried document. No candidate selection or trust is performed.
34    pub fn prepare(
35        provider: &'a P,
36        message: &'a SignedData<'a>,
37        requests: OwnedVec<ResolvedSigner<'a, P>>,
38        mode: ContentMode,
39        parent: &'operation Reader<'a>,
40    ) -> Result<Self, CheckError<P::Error>> {
41        const FIELD: &str = "signedData.encapContentInfo";
42        message
43            .require_session(parent, FIELD)
44            .map_err(CheckError::Processing)?;
45        let execution = parent.execution();
46        execution.charge_work(1).map_err(|cause| {
47            CheckError::Processing(Error::resource(
48                cause,
49                FIELD,
50                message.content().span().start,
51            ))
52        })?;
53        if (mode == ContentMode::Detached) != message.content().is_detached() {
54            return Err(CheckError::Processing(Error::schema(
55                ErrorCode::InvalidLocation,
56                FIELD,
57                message.content().span().start,
58            )));
59        }
60        for request in &requests {
61            execution.charge_work(1).map_err(|cause| {
62                CheckError::Processing(Error::resource(cause, FIELD, request.signer().span().start))
63            })?;
64            message
65                .require_signer(request.signer(), "signedData.signerInfos")
66                .map_err(CheckError::Processing)?;
67        }
68        let batch = Batch::prepare(provider, requests, &execution, FIELD, message.span().start)?;
69        Ok(Self {
70            message,
71            batch,
72            mode,
73            execution,
74        })
75    }
76
77    /// Consumes the ORIGINAL carried content through the same streaming batch.
78    /// The caller cannot replace it with a different byte source. Empty carried
79    /// content remains a real empty input, distinct from absence.
80    pub fn run_carried(
81        self,
82    ) -> Result<DocumentChecks<'a>, PassError<crate::octets::SourceError, P::Error>> {
83        let refusal = || {
84            crate::stream::StreamError::Processing(Error::schema(
85                ErrorCode::InvalidLocation,
86                "signedData.encapContentInfo",
87                self.message.content().span().start,
88            ))
89        };
90        if self.mode != ContentMode::Carried {
91            return Err(refusal());
92        }
93        let Some(mut content) = self.message.content().content() else {
94            return Err(refusal());
95        };
96        let checks = self.batch.run(&mut content, &self.execution)?;
97        Ok(DocumentChecks {
98            message: self.message,
99            checks,
100        })
101    }
102
103    /// Consumes the explicit caller input of the original detached `SignedData`.
104    /// This establishes that input's mathematical agreement with the signed
105    /// message-digest, not an application claim about where bytes were acquired.
106    pub fn run_detached<S: Source>(
107        self,
108        source: &mut S,
109    ) -> Result<DocumentChecks<'a>, PassError<S::Error, P::Error>> {
110        if self.mode != ContentMode::Detached || !self.message.content().is_detached() {
111            return Err(crate::stream::StreamError::Processing(Error::schema(
112                ErrorCode::InvalidLocation,
113                "signedData.encapContentInfo",
114                self.message.content().span().start,
115            )));
116        }
117        let checks = self.batch.run(source, &self.execution)?;
118        Ok(DocumentChecks {
119            message: self.message,
120            checks,
121        })
122    }
123}
124
125/// Original-container-bound mathematical facts for every selected root signer.
126/// A successful partial source pass never creates this receipt. Per-signer
127/// negative/unsupported results remain independent of application acceptance.
128pub struct DocumentChecks<'a> {
129    message: &'a SignedData<'a>,
130    checks: OwnedVec<Checked<'a>>,
131}
132
133impl<'a> DocumentChecks<'a> {
134    /// Same original document whose logical content this complete pass consumed.
135    #[must_use]
136    pub fn message(&self) -> &'a SignedData<'a> {
137        self.message
138    }
139    /// Every prepared selected root result in request order, without copying.
140    #[must_use]
141    pub fn checks(&self) -> &[Checked<'a>] {
142        &self.checks
143    }
144
145    /// Requires THIS actual batch result and original document in constant
146    /// metadata work. Equal bytes, equivalent signer values or another check
147    /// under the same account cannot supply its input-source association.
148    pub fn require<W: Work>(
149        &self,
150        message: &SignedData<'_>,
151        checked: &Checked<'_>,
152        execution: &W,
153    ) -> crate::Result<()> {
154        let at = checked.signer().span().start;
155        execution
156            .charge_work(1)
157            .map_err(|cause| Error::resource(cause, "signedData.signerInfos", at))?;
158        let address = core::ptr::from_ref(checked).cast::<Checked<'a>>();
159        if !core::ptr::eq(message, self.message) || !self.checks.as_ptr_range().contains(&address) {
160            return Err(Error::schema(
161                ErrorCode::InvalidLocation,
162                "signedData.signerInfos",
163                at,
164            ));
165        }
166        Ok(())
167    }
168}