1use crate::{
4 arena::Kind,
5 bytes::Form,
6 container::Container,
7 document::Document,
8 error::{Error, ErrorKind, ReadFailure, Syntax},
9 model::{admit, get, unsigned},
10 range::RangeSource,
11 signature::{Coverage, CoveredError, Signature, SubFilter},
12 source::Location,
13};
14use structured_asn1::{
15 Reader, Span,
16 memory::{Execution, HashWork, MemoryError, Work},
17};
18use structured_cms::{
19 provider::SignatureProvider,
20 timestamp::{
21 AsyncRawInput, CheckContext, CheckedTimestamp, ProfileCheckError, ProfileFailure,
22 ProfileSource, Token, TokenExtension,
23 },
24};
25use structured_ltv::{
26 source::Location as Original,
27 timestamp::{CheckedActor, Purpose, Verified},
28};
29use structured_pkix::{Certificate, path::WorkingKey};
30
31pub mod coverage;
32mod requirements;
33pub mod sources;
34pub use requirements::{DocumentTimestamp, Requirements, VriTimestamp};
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct Node {
39 carrier: Location,
40 member: Member,
41}
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43enum Member {
44 Pdf,
45 Cms(Span),
46 MissingContents,
47 Dss(usize),
48}
49impl Node {
50 #[must_use]
52 pub fn span(self) -> Span {
53 self.carrier.enclosing()
54 }
55 #[must_use]
57 pub fn pdf_location(self) -> Location {
58 self.carrier
59 }
60 #[must_use]
63 pub fn cms_span(self) -> Option<Span> {
64 match self.member {
65 Member::Cms(span) => Some(span),
66 Member::Pdf | Member::MissingContents | Member::Dss(_) => None,
67 }
68 }
69 #[must_use]
71 pub fn is_missing_contents(self) -> bool {
72 self.member == Member::MissingContents
73 }
74 #[must_use]
77 pub fn material_revision(self) -> Option<usize> {
78 match self.member {
79 Member::Dss(revision) => Some(revision),
80 _ => None,
81 }
82 }
83 pub(crate) fn of(location: Location) -> Self {
84 Self {
85 carrier: location,
86 member: Member::Pdf,
87 }
88 }
89 pub(crate) fn missing_contents(carrier: Location) -> Self {
90 Self {
91 carrier,
92 member: Member::MissingContents,
93 }
94 }
95 pub(crate) fn dss(carrier: Location, revision: usize) -> Self {
96 Self {
97 carrier,
98 member: Member::Dss(revision),
99 }
100 }
101 pub(crate) fn inside(carrier: Location, member: structured_cades::source::Node) -> Self {
102 Self {
103 carrier,
104 member: Member::Cms(member.span()),
105 }
106 }
107}
108impl HashWork for Node {
109 fn hash_work(&self) -> Result<u64, MemoryError> {
110 Ok(match (self.carrier.source(), self.member) {
111 (crate::source::Source::File, Member::Pdf | Member::MissingContents) => 4,
112 (crate::source::Source::File, Member::Cms(_)) => 6,
113 (crate::source::Source::Decoded { .. }, Member::Pdf | Member::MissingContents) => 8,
114 (crate::source::Source::Decoded { .. }, Member::Cms(_)) => 10,
115 (crate::source::Source::File, Member::Dss(_)) => 5,
116 (crate::source::Source::Decoded { .. }, Member::Dss(_)) => 9,
117 })
118 }
119}
120impl core::hash::Hash for Node {
121 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
122 match self.carrier.source() {
123 crate::source::Source::File => 0_u8.hash(state),
124 crate::source::Source::Decoded {
125 stream,
126 revision,
127 stage,
128 } => {
129 1_u8.hash(state);
130 stream.start.hash(state);
131 stream.length.hash(state);
132 revision.hash(state);
133 stage.hash(state);
134 }
135 }
136 self.carrier.span().start.hash(state);
137 self.carrier.span().length.hash(state);
138 match self.member {
139 Member::Pdf => 0_u8.hash(state),
140 Member::Cms(span) => {
141 1_u8.hash(state);
142 span.start.hash(state);
143 span.length.hash(state);
144 }
145 Member::MissingContents => 2_u8.hash(state),
146 Member::Dss(revision) => {
147 3_u8.hash(state);
148 revision.hash(state);
149 }
150 }
151 }
152}
153
154pub struct Pades {
156 _private: (),
157}
158pub(crate) const ISSUER: Pades = Pades { _private: () };
159
160pub(crate) fn profile(
161 input: &structured_cms::timestamp::ProfileInput<'_, '_, '_, '_>,
162) -> Result<(), ProfileFailure<structured_cades::Error>> {
163 let fail = |source, cause| ProfileFailure { source, cause };
167 let md5 = structured_asn1::Oid::from_bytes(&[42, 134, 72, 134, 247, 13, 2, 5]);
168 input
169 .parent
170 .charge(
171 1 + input
172 .token
173 .signer()
174 .digest_algorithm()
175 .algorithm()
176 .as_bytes()
177 .len() as u64,
178 )
179 .map_err(|cause| {
180 fail(
181 ProfileSource::Envelope,
182 structured_cades::Error::Encoding {
183 field: "pades.timestamp",
184 cause,
185 },
186 )
187 })?;
188 if input.token.signer().digest_algorithm().algorithm() == md5 {
189 return Err(fail(
190 ProfileSource::Envelope,
191 structured_cades::Error::Syntax {
192 code: structured_cms::ErrorCode::InvalidLocation,
193 field: "pades.timestamp.md5",
194 at: input.token.signer().span().start,
195 },
196 ));
197 }
198 if let structured_cms::timestamp::HashAlgorithmReading::Identifier(algorithm) =
199 input.info.imprint().algorithm()
200 {
201 input
202 .reader
203 .charge(1 + algorithm.algorithm().as_bytes().len() as u64)
204 .map_err(|cause| {
205 fail(
206 ProfileSource::Information,
207 structured_cades::Error::Encoding {
208 field: "pades.timestamp",
209 cause,
210 },
211 )
212 })?;
213 if algorithm.algorithm() == md5 {
214 return Err(fail(
215 ProfileSource::Information,
216 structured_cades::Error::Syntax {
217 code: structured_cms::ErrorCode::InvalidLocation,
218 field: "pades.timestamp.md5",
219 at: input.info.imprint().span().start,
220 },
221 ));
222 }
223 }
224 Ok(())
225}
226
227#[derive(Debug)]
229pub enum TimestampError<E, P, R> {
230 Pdf(ReadFailure<E>),
232 Check(ProfileCheckError<CoveredError<E>, P, R, structured_cades::Error>),
234}
235
236pub struct DocumentTime<'q, 'a, 's, 'd, D, G> {
239 signature: Signature<'a, 's, D, G>,
240 checks: CheckedTimestamp<'q>,
241 certificate: &'q Certificate<'d>,
242 key: WorkingKey<'d>,
243}
244
245impl<'q, 'a, 's, 'd, D: Copy + Eq, G: Copy + Eq> DocumentTime<'q, 'a, 's, 'd, D, G> {
246 #[must_use]
248 pub fn checks(&self) -> &CheckedTimestamp<'q> {
249 &self.checks
250 }
251 #[must_use]
253 pub fn signature(&self) -> Signature<'a, 's, D, G> {
254 self.signature
255 }
256
257 pub fn into_actor(
262 self,
263 covered: Signature<'_, 's, D, G>,
264 operation: &impl Work,
265 ) -> crate::Result<Option<CheckedActor<'q, 'd, Node, D, G, Pades>>> {
266 if !self.associated(covered, operation)? {
267 return Ok(None);
268 }
269 let location = self.signature.location();
270 let owner = self.signature.owner();
271 let work = owner.value.session.query(operation);
272 let Coverage::Revision(archive) = self.signature.coverage() else {
273 return Ok(None);
274 };
275 let Coverage::Revision(subject) = covered.coverage() else {
276 return Ok(None);
277 };
278 if subject > archive || covered.contents().source() != crate::source::Source::File {
279 return Ok(None);
280 }
281 let needed = covered.contents().span();
282 let mut complete = false;
283 for span in self.signature.ranges() {
284 admit(&work, location, 1)?;
285 if span.start <= needed.start
286 && span.start + span.length >= needed.start + needed.length
287 {
288 complete = true;
289 break;
290 }
291 }
292 if !complete {
293 return Ok(None);
294 }
295 Ok(self.issue(covered))
296 }
297
298 pub fn into_historical_actor(
302 self,
303 root: Signature<'_, 's, D, G>,
304 operation: &impl Work,
305 ) -> crate::Result<Option<CheckedActor<'q, 'd, Node, D, G, Pades>>> {
306 if !self.associated(root, operation)? {
307 return Ok(None);
308 }
309 Ok(self.issue(root))
310 }
311
312 fn associated(
313 &self,
314 root: Signature<'_, 's, D, G>,
315 operation: &impl Work,
316 ) -> crate::Result<bool> {
317 let owner = self.signature.owner();
318 let target = root.owner();
319 let work = owner.value.session.query(operation);
320 let location = self.signature.location();
321 admit(&work, location, 1)?;
322 if !core::ptr::eq(owner.value.session, target.value.session)
323 || owner.value.stamp != target.value.stamp
324 {
325 return Err(Error {
326 kind: ErrorKind::Foreign,
327 location,
328 });
329 }
330 Ok(matches!(self.signature.coverage(), Coverage::Revision(_))
331 && matches!(root.coverage(), Coverage::Revision(_)))
332 }
333
334 fn issue(
335 self,
336 covered: Signature<'_, 's, D, G>,
337 ) -> Option<CheckedActor<'q, 'd, Node, D, G, Pades>> {
338 let location = self.signature.location();
339 let owner = self.signature.owner();
340 let evidence = self.checks.into_evidence()?;
341 let property = Node::of(location);
342 let verified = Verified::new(
343 ISSUER,
344 Original {
345 snapshot: owner.value.stamp.snapshot,
346 node: Node::of(self.signature.contents()),
347 },
348 Node::of(covered.location()),
349 property,
350 Purpose::Archive,
351 evidence,
352 );
353 Some(CheckedActor::new(
354 ISSUER,
355 verified,
356 self.certificate,
357 self.key,
358 ))
359 }
360}
361
362impl<'s, S: RangeSource> Document<'s, S> {
363 #[expect(
372 clippy::result_large_err,
373 reason = "timestamp and profile failures retain original mapped context and opaque causes without an error-path allocation"
374 )]
375 pub async fn check_timestamp<'q, 'a, 'token, 'certificate, P, PE, RE>(
376 &'q self,
377 container: &'q Container<'a, 's, S::Document, S::Generation>,
378 token: &'q Token<'token>,
379 provider: &'q P,
380 parent: &Reader<'q>,
381 context: &'q CheckContext<'certificate, 'q>,
382 recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
383 ) -> Result<
384 DocumentTime<'q, 'a, 's, 'certificate, S::Document, S::Generation>,
385 TimestampError<S::Error, PE, RE>,
386 >
387 where
388 'token: 'q,
389 'certificate: 'q,
390 's: 'q,
391 'a: 'q,
392 P: for<'scope> SignatureProvider<'scope, Error = PE>,
393 {
394 let pdf = TimestampError::Pdf;
395 let signature = container.signature();
396 let location = signature.location();
397 let work = parent.execution();
398 container
399 .require(token.message(), parent)
400 .map_err(|cause| pdf(cause.into()))?;
401 self.check(&work, location)
402 .map_err(|cause| pdf(cause.into()))?;
403 let malformed = malformed_timestamp::<S::Error, PE, RE>;
404 if !signature.is_document_timestamp()
405 || signature.sub_filter() != Some(SubFilter::Rfc3161)
406 || !matches!(signature.coverage(), Coverage::Revision(_))
407 {
408 return Err(malformed(location));
409 }
410 let reading = signature.owner();
411 let arena = &reading.value.arena;
412 let node = signature.node();
413 crate::direct::visit(arena, node, &work, |location| Err(malformed(location))).map_err(
414 |cause| match cause {
415 crate::direct::VisitError::Processing(cause) => pdf(cause.into()),
416 crate::direct::VisitError::Callback(cause) => cause,
417 },
418 )?;
419 let filter = get(arena, node, b"Filter", &work).map_err(|cause| pdf(cause.into()))?;
420 if filter.is_none_or(|node| !matches!(arena.nodes[node.0].kind, Kind::Name(_))) {
421 return Err(malformed(location));
422 }
423 let Coverage::Revision(revision) = signature.coverage() else {
424 return Err(malformed(location));
425 };
426 let graph = self.graph(revision, &work).await.map_err(pdf)?;
427 let fields = self.fields(&graph, &work).await.map_err(pdf)?;
428 let transforms = self
429 .transforms(signature, &graph, &work)
430 .await
431 .map_err(pdf)?;
432 let placement = self
433 .signature_placement(signature, &graph, &fields, &transforms, &work)
434 .map_err(pdf)?;
435 if !placement.all_requirements_met() {
436 return Err(malformed(location));
437 }
438 if !reading.value.faults().is_empty()
439 || !matches!(
440 arena.nodes[get(arena, node, b"Contents", &work)
441 .map_err(|cause| pdf(cause.into()))?
442 .expect("inspected Contents")
443 .0]
444 .kind,
445 Kind::String(Form::Hexadecimal)
446 )
447 {
448 return Err(malformed(signature.contents()));
449 }
450 if let Some(version) = get(arena, node, b"V", &work).map_err(|cause| pdf(cause.into()))?
451 && unsigned(arena, version, &work).map_err(|cause| pdf(cause.into()))? != 0
452 {
453 return Err(malformed(arena.nodes[version.0].location));
454 }
455 for key in [
456 b"Cert".as_slice(),
457 b"Reference",
458 b"Changes",
459 b"R",
460 b"Prop_AuthTime",
461 b"Prop_AuthType",
462 ] {
463 if let Some(value) = get(arena, node, key, &work).map_err(|cause| pdf(cause.into()))? {
464 return Err(malformed(arena.nodes[value.0].location));
465 }
466 }
467 let mut covered = self.covered(signature, &work).map_err(pdf)?;
468 let checks = token
469 .check_historical_async_with_profile(
470 provider,
471 parent,
472 context,
473 &mut AsyncRawInput::new(&mut covered),
474 recognizes,
475 |input| profile(&input),
476 )
477 .await
478 .map_err(TimestampError::Check)?;
479 self.check(&work, location)
480 .map_err(|cause| pdf(cause.into()))?;
481 Ok(DocumentTime {
482 signature,
483 checks,
484 certificate: context.certificate,
485 key: context.key,
486 })
487 }
488}
489
490fn malformed_timestamp<S, P, R>(location: crate::source::Location) -> TimestampError<S, P, R> {
491 TimestampError::Pdf(
492 Error {
493 kind: ErrorKind::Syntax(Syntax::Container),
494 location,
495 }
496 .into(),
497 )
498}