1use crate::{
4 arena::Kind,
5 bytes::Form,
6 container::Container,
7 date::Date,
8 document::Document,
9 error::{Error, ErrorKind, ReadFailure},
10 model::{admit, get},
11 range::RangeSource,
12 signature::{Coverage, SubFilter},
13 source::Location,
14};
15use structured_asn1::{
16 Reader,
17 memory::{Memory, OwnedVec, Work},
18};
19use structured_cades::prepare::{Preparation as CmsPreparation, Rule as CmsRule, Severity};
20use structured_cms::oid;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
24pub enum Level {
25 Basic,
27 Timestamped,
29 LongTerm,
31 Archival,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum Rule {
38 Signature,
40 Field(&'static str),
42 Forbidden,
44 Cms,
46 Reason,
48 ClaimedTime,
50 SignaturePlacement,
52}
53
54#[derive(Debug, Clone, Copy)]
56pub struct Finding {
57 pub location: Location,
59 pub rule: Rule,
61}
62
63pub struct Preparation<'p, 'm, 'd, 'a, 's, D, G> {
67 pub(crate) container: &'p Container<'a, 's, D, G>,
68 pub(crate) cms: &'p CmsPreparation<'m, 'd>,
69 requested: Level,
70 claimed: Option<Date>,
71 placement: Option<crate::permissions::Placement>,
72 findings: OwnedVec<Finding, Memory>,
73}
74
75#[derive(Debug)]
77pub enum PrepareError<E> {
78 Pdf(ReadFailure<E>),
80 Cms(structured_cades::Error),
82}
83
84impl<'p, 'm, 'd, 'a, 's, D: Copy + Eq, G: Copy + Eq> Preparation<'p, 'm, 'd, 'a, 's, D, G> {
85 async fn read_placement<S: RangeSource<Document = D, Generation = G>>(
86 document: &Document<'s, S>,
87 signature: crate::signature::Signature<'a, 's, D, G>,
88 work: &impl Work,
89 record: &mut impl FnMut(Rule, Location) -> Result<(), PrepareError<S::Error>>,
90 ) -> Result<Option<crate::permissions::Placement>, PrepareError<S::Error>> {
91 let pdf = PrepareError::Pdf;
92 let location = signature.location();
93 let placement = if let Coverage::Revision(revision) = signature.coverage() {
94 let graph = document.graph(revision, work).await.map_err(pdf)?;
95 let fields = document.fields(&graph, work).await.map_err(pdf)?;
96 let transforms = document
97 .transforms(signature, &graph, work)
98 .await
99 .map_err(pdf)?;
100 let placement = document
101 .signature_placement(signature, &graph, &fields, &transforms, work)
102 .map_err(pdf)?;
103 if !placement.all_requirements_met() {
104 record(Rule::SignaturePlacement, location)?;
105 }
106 for node in &graph.nodes {
107 admit(work, location, 1).map_err(|cause| pdf(cause.into()))?;
108 if let Some(reading) = &node.reading {
109 for fault in reading.value.faults() {
110 admit(work, fault.location, 1).map_err(|cause| pdf(cause.into()))?;
111 record(Rule::Signature, fault.location)?;
112 }
113 }
114 }
115 Some(placement)
116 } else {
117 None
118 };
119 Ok(placement)
120 }
121
122 async fn read_pdf_fields<S: RangeSource<Document = D, Generation = G>>(
123 document: &Document<'s, S>,
124 signature: crate::signature::Signature<'a, 's, D, G>,
125 work: &impl Work,
126 record: &mut impl FnMut(Rule, Location) -> Result<(), PrepareError<S::Error>>,
127 ) -> Result<Option<Date>, PrepareError<S::Error>> {
128 let pdf = PrepareError::Pdf;
129 let location = signature.location();
130 let reading = signature.owner();
131 let arena = &reading.value.arena;
132 let node = signature.node();
133 crate::direct::visit(arena, node, work, |location| {
134 record(Rule::Signature, location)
135 })
136 .map_err(|cause| match cause {
137 crate::direct::VisitError::Processing(cause) => pdf(cause.into()),
138 crate::direct::VisitError::Callback(cause) => cause,
139 })?;
140 for fault in &arena.faults {
141 admit(work, fault.location, 1).map_err(|cause| pdf(cause.into()))?;
142 record(Rule::Signature, fault.location)?;
143 }
144 if !matches!(
145 arena.nodes[get(arena, node, b"Contents", work)
146 .map_err(|cause| pdf(cause.into()))?
147 .expect("inspected Contents")
148 .0]
149 .kind,
150 Kind::String(Form::Hexadecimal)
151 ) {
152 record(Rule::Field("Contents"), signature.contents())?;
153 }
154 let filter = get(arena, node, b"Filter", work).map_err(|cause| pdf(cause.into()))?;
155 if filter.is_none_or(|node| !matches!(arena.nodes[node.0].kind, Kind::Name(_))) {
156 record(Rule::Field("Filter"), location)?;
157 }
158 if get(arena, node, b"Cert", work)
159 .map_err(|cause| pdf(cause.into()))?
160 .is_some()
161 {
162 record(Rule::Forbidden, location)?;
163 }
164 let date = get(arena, node, b"M", work).map_err(|cause| pdf(cause.into()))?;
165 let claimed = if let Some(date) = date {
166 let date_location = arena.nodes[date.0].location;
167 let bytes = document
168 .string(reading, date, 22, work)
169 .await
170 .map_err(pdf)?;
171 match Date::read(&bytes, date_location, work) {
172 Ok(date) => {
173 if date.offset_minutes() != 0 {
174 record(Rule::ClaimedTime, date_location)?;
175 }
176 Some(date)
177 }
178 Err(cause)
179 if matches!(
180 cause.kind,
181 ErrorKind::Resource(_) | ErrorKind::Foreign | ErrorKind::Limit(_)
182 ) =>
183 {
184 return Err(pdf(cause.into()));
185 }
186 Err(_) => {
187 record(Rule::ClaimedTime, arena.nodes[date.0].location)?;
188 None
189 }
190 }
191 } else {
192 record(Rule::Field("M"), location)?;
193 None
194 };
195 for key in [b"Location".as_slice(), b"Reason", b"Name", b"ContactInfo"] {
196 if let Some(value) = get(arena, node, key, work).map_err(|cause| pdf(cause.into()))? {
197 let bytes = document
198 .string(reading, value, usize::MAX, work)
199 .await
200 .map_err(pdf)?;
201 crate::text::read(&bytes, arena.nodes[value.0].location, work)
202 .map_err(|cause| pdf(cause.into()))?;
203 }
204 }
205 Ok(claimed)
206 }
207
208 fn read_cms_fields<E>(
209 cms: &CmsPreparation<'m, 'd>,
210 signature: crate::signature::Signature<'a, 's, D, G>,
211 work: &impl Work,
212 record: &mut impl FnMut(Rule, Location) -> Result<(), PrepareError<E>>,
213 ) -> Result<(), PrepareError<E>> {
214 let pdf = PrepareError::Pdf;
215 let location = signature.location();
216 let reading = signature.owner();
217 let arena = &reading.value.arena;
218 let node = signature.node();
219 for finding in cms.findings() {
220 admit(work, signature.contents(), 1).map_err(|cause| pdf(cause.into()))?;
221 if finding.severity == Severity::Advisory {
222 continue;
223 }
224 if matches!(finding.rule, CmsRule::Missing(kind) if kind == oid::SIGNING_TIME)
225 || matches!(
226 finding.rule,
227 CmsRule::SignatureTimestamp | CmsRule::ArchiveTimestamp
228 )
229 {
230 continue;
231 }
232 record(Rule::Cms, signature.contents())?;
233 }
234 let reason = get(arena, node, b"Reason", work)
235 .map_err(|cause| pdf(cause.into()))?
236 .is_some();
237 for set in [
238 cms.signer().signed_attributes(),
239 cms.signer().unsigned_attributes(),
240 ]
241 .into_iter()
242 .flatten()
243 {
244 for attribute in set.items() {
245 admit(
246 work,
247 signature.contents(),
248 attribute.kind().as_bytes().len() as u64 + 1,
249 )
250 .map_err(|cause| pdf(cause.into()))?;
251 let kind = attribute.kind();
252 if reason
253 && (kind == oid::COMMITMENT_TYPE_INDICATION
254 || kind == oid::SIGNATURE_POLICY_IDENTIFIER)
255 {
256 record(Rule::Reason, location)?;
257 }
258 if structured_cades::properties::defines(kind, work).map_err(|cause| {
259 pdf(Error {
260 kind: ErrorKind::Resource(cause),
261 location: signature.contents(),
262 }
263 .into())
264 })? && !allowed(kind)
265 {
266 record(Rule::Forbidden, signature.contents())?;
267 }
268 }
269 }
270 Ok(())
271 }
272
273 pub async fn read<S: RangeSource<Document = D, Generation = G>, W: Work>(
281 document: &Document<'s, S>,
282 container: &'p Container<'a, 's, D, G>,
283 cms: &'p CmsPreparation<'m, 'd>,
284 requested: Level,
285 parent: &Reader<'d>,
286 operation: &W,
287 ) -> Result<Self, PrepareError<S::Error>> {
288 let pdf = PrepareError::Pdf;
289 let message = cms.document().message();
290 container
291 .require(message, parent)
292 .map_err(|cause| pdf(cause.into()))?;
293 message
294 .require_session(parent, "pades")
295 .map_err(|cause| PrepareError::Cms(cause.into()))?;
296 let signature = container.signature();
297 let location = signature.location();
298 let work = document.session.query(operation);
299 document
300 .check(&work, location)
301 .map_err(|cause| pdf(cause.into()))?;
302 let mut findings = work.vector();
303 let mut record = |rule, location| {
304 findings
305 .try_push(Finding { location, rule })
306 .map_err(|cause| {
307 pdf(Error {
308 kind: ErrorKind::Resource(cause),
309 location,
310 }
311 .into())
312 })
313 };
314 if signature.sub_filter() != Some(SubFilter::CadesDetached)
315 || signature.is_document_timestamp()
316 || !matches!(signature.coverage(), Coverage::Revision(_))
317 {
318 record(Rule::Signature, location)?;
319 }
320 if message.signers().len() != 1 || !message.content().is_detached() {
321 record(Rule::Signature, signature.contents())?;
322 }
323 let placement = Self::read_placement(document, signature, &work, &mut record).await?;
324 let claimed = Self::read_pdf_fields(document, signature, &work, &mut record).await?;
325 Self::read_cms_fields(cms, signature, &work, &mut record)?;
326 document
327 .check(&work, location)
328 .map_err(|cause| pdf(cause.into()))?;
329 Ok(Self {
330 container,
331 cms,
332 requested,
333 claimed,
334 placement,
335 findings: findings.into_owned(),
336 })
337 }
338
339 #[must_use]
341 pub fn requested(&self) -> Level {
342 self.requested
343 }
344 #[must_use]
346 pub fn claimed_time(&self) -> Option<Date> {
347 self.claimed
348 }
349 #[must_use]
351 pub fn placement(&self) -> Option<&crate::permissions::Placement> {
352 self.placement.as_ref()
353 }
354 #[must_use]
356 pub fn findings(&self) -> &[Finding] {
357 &self.findings
358 }
359 #[must_use]
361 pub fn all_basic_format_requirements_met(&self) -> bool {
362 self.findings.is_empty()
363 }
364 #[must_use]
366 pub fn cms(&self) -> &'p CmsPreparation<'m, 'd> {
367 self.cms
368 }
369}
370
371fn allowed(kind: structured_asn1::Oid<'_>) -> bool {
372 [
374 oid::CONTENT_TYPE,
375 oid::MESSAGE_DIGEST,
376 oid::SIGNER_ATTRIBUTES_V2,
377 oid::CONTENT_TIMESTAMP,
378 oid::SIGNATURE_POLICY_IDENTIFIER,
379 oid::COMMITMENT_TYPE_INDICATION,
380 oid::SIGNING_CERTIFICATE,
381 oid::SIGNING_CERTIFICATE_V2,
382 oid::SIGNATURE_TIMESTAMP,
383 ]
384 .contains(&kind)
385}