File Coverage

blib/lib/Crypt/TimestampedData.pm
Criterion Covered Total %
statement 122 167 73.0
branch 43 114 37.7
condition 8 37 21.6
subroutine 13 15 86.6
pod 12 12 100.0
total 198 345 57.3


line stmt bran cond sub pod time code
1             package Crypt::TimestampedData;
2             $Crypt::TimestampedData::VERSION = '0.01';
3 10     10   796589 use strict;
  10         19  
  10         333  
4 10     10   42 use warnings;
  10         16  
  10         462  
5              
6 10     10   4411 use Convert::ASN1;
  10         425597  
  10         22930  
7              
8             =pod
9              
10             =head1 NAME
11              
12             Crypt::TimestampedData - Read and write TimeStampedData files (.TSD, RFC 5544)
13              
14             =head1 VERSION
15              
16             version 0.01
17              
18             =head1 SYNOPSIS
19              
20             use Crypt::TimestampedData;
21              
22             # Decode from .TSD file
23             my $tsd = Crypt::TimestampedData->read_file('/path/file.tsd');
24             my $version = $tsd->{version};
25             my $data_uri = $tsd->{dataUri}; # optional
26             my $meta = $tsd->{metaData}; # optional
27             my $content_der = $tsd->{content}; # optional (CMS ContentInfo DER)
28             my $evidence_content = $tsd->{temporalEvidence};
29              
30             # Encode to .TSD file
31             Crypt::TimestampedData->write_file('/path/out.tsd', $tsd);
32              
33             =head1 DESCRIPTION
34              
35             Minimal implementation of the TimeStampedData format (RFC 5544) using Convert::ASN1.
36             This version treats CMS constructs and TimeStampTokens as opaque DER blobs.
37             The goal is to enable reading/writing of .TSD files, delegating CMS/TS handling
38             to external libraries when available.
39              
40             =head1 METHODS
41              
42             =head2 new(%args)
43              
44             Creates a new Crypt::TimestampedData object with the provided arguments.
45              
46             =head2 read_file($filepath)
47              
48             Reads and decodes a .TSD file from the specified path. Returns a hash reference
49             containing the decoded TimeStampedData structure.
50              
51             =head2 write_file($filepath, $tsd_hashref)
52              
53             Encodes and writes a TimeStampedData structure to the specified file path.
54              
55             =head2 decode_der($der)
56              
57             Decodes DER-encoded TimeStampedData. Handles both direct TSD format and
58             CMS ContentInfo wrappers (id-ct-TSTData and pkcs7-signedData).
59              
60             =head2 encode_der($tsd_hashref)
61              
62             Encodes a TimeStampedData hash reference to DER format.
63              
64             =head2 extract_content_der($tsd_hashref)
65              
66             Extracts the embedded original content from TimeStampedData.content.
67             Returns raw bytes of the original file if available, otherwise undef.
68              
69             =head2 extract_tst_tokens_der($tsd_hashref)
70              
71             Extracts RFC 3161 TimeStampToken(s) as DER ContentInfo blobs.
72             Returns array reference of DER-encoded ContentInfo tokens.
73              
74             =head2 write_content_file($tsd_hashref, $filepath)
75              
76             Convenience method to write extracted content to a file.
77              
78             =head2 extract_signed_content_bytes($tsd_hashref)
79              
80             Extracts encapsulated content from a SignedData (p7m) stored in TSD.content.
81             Returns raw bytes of the signed payload (eContent) when available.
82              
83             =head2 write_signed_content_file($tsd_hashref, $filepath)
84              
85             Convenience method to write extracted signed content to a file.
86              
87             =head2 write_tst_files($tsd_hashref, $dirpath)
88              
89             Writes extracted timestamp tokens to individual .tsr files in the specified directory.
90              
91             =head2 write_tds($marked_filepath, $tsr_input, $out_filepath_opt)
92              
93             Creates and writes a TSD file from a marked file and one or more RFC3161
94             TimeStampToken(s) provided as .TSR (DER CMS ContentInfo) blobs or paths.
95              
96             =head1 EXAMPLES
97              
98             # Read a TSD file
99             my $tsd = Crypt::TimestampedData->read_file('document.tsd');
100             print "Version: $tsd->{version}\n";
101            
102             # Extract the original content
103             Crypt::TimestampedData->write_content_file($tsd, 'original_document.pdf');
104            
105             # Extract timestamp tokens
106             my $tokens = Crypt::TimestampedData->extract_tst_tokens_der($tsd);
107             print "Found " . scalar(@$tokens) . " timestamp tokens\n";
108            
109             # Create a new TSD file
110             my $output = Crypt::TimestampedData->write_tds(
111             'document.pdf', # original file
112             'timestamp.tsr', # timestamp token
113             'document.tsd' # output file
114             );
115              
116             =head1 COMMAND-LINE SCRIPTS
117              
118             This distribution includes several command-line scripts for working with TimeStampedData files:
119              
120             =over 4
121              
122             =item * C - Create a .tsd file from a file and timestamp
123              
124             =item * C - Extract content and timestamps from a .tsd file
125              
126             =item * C - Display information about a .tsd file
127              
128             =back
129              
130             =head2 Windows Usage
131              
132             On Windows systems, scripts must be executed with Perl explicitly:
133              
134             perl tsd-create --help
135             perl tsd-extract document.tsd
136             perl tsd-info document.tsd
137              
138             =head2 Unix/Linux Usage
139              
140             On Unix/Linux systems, scripts can be executed directly:
141              
142             tsd-create --help
143             tsd-extract document.tsd
144             tsd-info document.tsd
145              
146             =head1 REQUIREMENTS
147              
148             =over 4
149              
150             =item * Convert::ASN1
151              
152             =back
153              
154             =head1 AUTHOR
155              
156             Guido Brugnara - created with AI support.
157              
158             =head1 LICENSE
159              
160             This module is released under the same terms as Perl itself.
161              
162             =head1 SEE ALSO
163              
164             =over 4
165              
166             =item * RFC 5544 - TimeStampedData Format
167              
168             =item * RFC 3161 - Internet X.509 Public Key Infrastructure Time-Stamp Protocol
169              
170             =item * RFC 5652 - Cryptographic Message Syntax (CMS)
171              
172             =back
173              
174             =cut
175              
176             my $ASN1_SPEC = <<'__ASN1_RFC5544__';
177             TimeStampedData ::= SEQUENCE {
178             version INTEGER,
179             dataUri IA5String OPTIONAL,
180             metaData MetaData OPTIONAL,
181             content OCTET STRING OPTIONAL, -- ContentInfo DER (RFC 5652)
182             temporalEvidence Evidence
183             }
184              
185             MetaData ::= SEQUENCE {
186             hashProtected BOOLEAN,
187             fileName UTF8String OPTIONAL,
188             mediaType IA5String OPTIONAL,
189             otherMetaData ANY OPTIONAL
190             }
191              
192             Evidence ::= CHOICE {
193             tstEvidence [0] TimeStampTokenEvidence, -- see RFC 3161
194             ersEvidence [1] EvidenceRecord, -- see RFC 4998
195             otherEvidence [2] OtherEvidence
196             }
197              
198             OtherEvidence ::= SEQUENCE {
199             oeType OBJECT IDENTIFIER,
200             oeValue ANY DEFINED BY oeType }
201              
202             TimeStampTokenEvidence ::=
203             SEQUENCE OF TimeStampAndCRL
204              
205             TimeStampAndCRL ::= SEQUENCE {
206             timeStamp TimeStampToken, -- according to RFC 3161
207             crl CertificateList OPTIONAL -- according to RFC 5280
208             }
209              
210             TimeStampToken ::= ContentInfo
211              
212             CertificateList ::= ANY
213              
214             Attribute ::= ANY
215              
216             EvidenceRecord ::= ANY
217             -- OtherEvidence payload defined above (oeType, oeValue)
218              
219             -- Minimal CMS ContentInfo wrapper (RFC 5652)
220             ContentInfo ::= SEQUENCE {
221             contentType OBJECT IDENTIFIER,
222             content [0] EXPLICIT ANY OPTIONAL
223             }
224              
225              
226             -- Helper to unwrap OCTET STRING containers when needed
227             OctetString ::= OCTET STRING
228              
229             -- Minimal CMS structures to navigate SignedData → EncapsulatedContentInfo
230             SignedData ::= SEQUENCE {
231             version INTEGER,
232             digestAlgorithms SET OF AlgorithmIdentifier,
233             encapContentInfo EncapsulatedContentInfo,
234             certificates [0] IMPLICIT ANY OPTIONAL,
235             crls [1] IMPLICIT ANY OPTIONAL,
236             signerInfos SET OF ANY
237             }
238              
239             EncapsulatedContentInfo ::= SEQUENCE {
240             eContentType OBJECT IDENTIFIER,
241             eContent [0] EXPLICIT OCTET STRING OPTIONAL
242             }
243              
244             AlgorithmIdentifier ::= SEQUENCE {
245             algorithm OBJECT IDENTIFIER,
246             parameters ANY OPTIONAL
247             }
248              
249             __ASN1_RFC5544__
250              
251             my $ASN1 = Convert::ASN1->new;
252             $ASN1->prepare($ASN1_SPEC) or die "TSD ASN.1 prepare error: " . $ASN1->error;
253              
254             my $TSD_CODEC = $ASN1->find('TimeStampedData')
255             or die 'ASN.1 type TimeStampedData not found';
256              
257             my $CONTENTINFO_CODEC = $ASN1->find('ContentInfo')
258             or die 'ASN.1 type ContentInfo not found';
259              
260             my $OCTETSTRING_CODEC = $ASN1->find('OctetString')
261             or die 'ASN.1 type OctetString not found';
262              
263             my $SIGNEDDATA_CODEC = $ASN1->find('SignedData')
264             or die 'ASN.1 type SignedData not found';
265              
266             my $CONTENTINFO_TSD_CODEC = $ASN1->find('ContentInfoTSD')
267             or undef;
268              
269             my $OID_CT_TSTDATA = '1.2.840.113549.1.9.16.1.31';
270             my $OID_CT_SIGNEDDATA = '1.2.840.113549.1.7.2';
271              
272             sub new {
273 2     2 1 339308 my ($class, %args) = @_;
274 2         6 my $self = {%args};
275 2         12 return bless $self, $class;
276             }
277              
278             sub decode_der {
279 9     9 1 2381 my ($class, $der) = @_;
280              
281             # Try direct TimeStampedData first
282 9         65 my $decoded = $TSD_CODEC->decode($der);
283 9 100       2811 return $decoded if defined $decoded;
284              
285             # If that fails, try unwrapping CMS ContentInfo (common packaging for TSD)
286 5         21 my $ci = $CONTENTINFO_CODEC->decode($der);
287 5 50       1324 die 'ASN.1 decode failed: ' . $TSD_CODEC->error unless defined $ci;
288              
289             # Two acceptable wrappings for RFC 5544:
290             # 1) ContentInfo with contentType id-ct-TSTData and [0] content = TimeStampedData
291             # 2) ContentInfo with contentType pkcs7-signedData, whose encapContentInfo.eContentType is id-ct-TSTData
292              
293 5         15 my $content_der = $ci->{content};
294 5 50       19 die 'ASN.1 decode failed: ContentInfo without [0] content' unless defined $content_der;
295              
296             # Some encoders place TSD directly as [0] EXPLICIT SEQUENCE; others wrap as OCTET STRING
297 5 50       41 my $first_tag = length($content_der) ? ord(substr($content_der, 0, 1)) : -1;
298 5 50       47 if ($first_tag == 0x04) { # OCTET STRING
299 0         0 my $octets = $OCTETSTRING_CODEC->decode($content_der);
300 0 0       0 die 'ASN.1 decode failed: cannot unwrap OCTET STRING: ' . $OCTETSTRING_CODEC->error unless defined $octets;
301 0         0 $content_der = $octets;
302             }
303              
304             # Case 1: Direct id-ct-TSTData wrapper — decode TSD from [0] content (may be OCTET STRING)
305 5 50 33     43 if (defined $ci->{contentType} && $ci->{contentType} eq $OID_CT_TSTDATA) {
306 5         20 my $tsd = $TSD_CODEC->decode($content_der);
307 5 50       4281 die 'ASN.1 decode failed (inside ContentInfo/id-ct-TSTData): ' . $TSD_CODEC->error unless defined $tsd;
308 5         60 return $tsd;
309             }
310              
311             # Case 2: SignedData → EncapsulatedContentInfo with id-ct-TSTData
312 0 0 0     0 if (defined $ci->{contentType} && $ci->{contentType} eq $OID_CT_SIGNEDDATA) {
313 0         0 my $sd = $SIGNEDDATA_CODEC->decode($content_der);
314 0 0       0 die 'ASN.1 decode failed: cannot decode SignedData: ' . $SIGNEDDATA_CODEC->error unless defined $sd;
315 0   0     0 my $eci = $sd->{encapContentInfo} || {};
316 0         0 my $econtent_type = $eci->{eContentType};
317 0         0 my $econtent = $eci->{eContent};
318 0 0       0 die 'ASN.1 decode failed: SignedData without encapContentInfo.eContent' unless defined $econtent;
319 0 0 0     0 unless (defined $econtent_type && $econtent_type eq $OID_CT_TSTDATA) {
320 0         0 die "Input is CMS SignedData with eContentType '$econtent_type', expected id-ct-TSTData ($OID_CT_TSTDATA).";
321             }
322             # eContent is an OCTET STRING wrapping the TSD DER
323 0         0 my $tsd = $TSD_CODEC->decode($econtent);
324 0 0       0 die 'ASN.1 decode failed (inside SignedData/id-ct-TSTData): ' . $TSD_CODEC->error unless defined $tsd;
325 0         0 return $tsd;
326             }
327              
328 0         0 die "Input ContentInfo has unsupported contentType '$ci->{contentType}', expected id-ct-TSTData ($OID_CT_TSTDATA) or pkcs7-signedData ($OID_CT_SIGNEDDATA).";
329             }
330              
331             sub encode_der {
332 5     5 1 130322 my ($class, $tsd_hashref) = @_;
333 5         34 my $der = $TSD_CODEC->encode($tsd_hashref);
334 5 50       1647 die 'ASN.1 encode failed: ' . $TSD_CODEC->error unless defined $der;
335 5         34 return $der;
336             }
337              
338             sub read_file {
339 6     6 1 5673 my ($class, $filepath) = @_;
340 6 50       303 open my $fh, '<:raw', $filepath or die "Cannot open TSD file '$filepath' for reading: $!";
341 6         34 local $/; my $der = <$fh>; close $fh;
  6         248  
  6         76  
342 6         79 return $class->decode_der($der);
343             }
344              
345             sub write_file {
346 1     1 1 8224 my ($class, $filepath, $tsd_hashref) = @_;
347 1         4 my $der = $class->encode_der($tsd_hashref);
348 1 50       162 open my $fh, '>:raw', $filepath or die "Cannot open TSD file '$filepath' for writing: $!";
349 1         3 print {$fh} $der;
  1         15  
350 1         40 close $fh;
351 1         10 return 1;
352             }
353              
354             # Extract embedded original content from TimeStampedData.content
355             # Returns raw bytes of the original file if available, otherwise undef
356             sub extract_content_der {
357 7     7 1 6497 my ($class, $tsd_hashref) = @_;
358 7         23 my $content_der = $tsd_hashref->{content};
359 7 100       34 return undef unless defined $content_der;
360              
361             # content is a CMS ContentInfo (DER). Try to unwrap eContent when present
362 6         25 my $ci = $CONTENTINFO_CODEC->decode($content_der);
363 6 50       1491 return $content_der unless defined $ci; # if decoding fails, return raw blob (treat as opaque)
364              
365 6         17 my $content_type = $ci->{contentType};
366 6         16 my $econtent = $ci->{content};
367 6 50       18 return undef unless defined $econtent; # no encapsulated content
368              
369             # If the ContentInfo is id-data, unwrap to the raw bytes; otherwise
370             # (e.g., SignedData/p7m), return the full ContentInfo DER as the original file
371 6 50 33     52 if (defined $content_type && $content_type eq '1.2.840.113549.1.7.1') { # id-data
372 6 50       75 my $first_tag = length($econtent) ? ord(substr($econtent, 0, 1)) : -1;
373 6 50       21 if ($first_tag == 0x04) {
374 6         24 my $octets = $OCTETSTRING_CODEC->decode($econtent);
375 6 50       547 return $octets if defined $octets;
376             }
377 0         0 return $econtent; # fallback: return inner content as-is
378             }
379              
380             # For SignedData and others: preserve outer ContentInfo (p7m)
381 0         0 return $content_der;
382             }
383              
384             # Extract RFC 3161 TimeStampToken(s) as DER ContentInfo blobs
385             # Returns arrayref of DER-encoded ContentInfo tokens
386             sub extract_tst_tokens_der {
387 4     4 1 2913 my ($class, $tsd_hashref) = @_;
388 4         7 my @tokens_der;
389              
390 4         10 my $evidence = $tsd_hashref->{temporalEvidence};
391 4 50       13 return \@tokens_der unless defined $evidence;
392              
393 4 50       43 if (exists $evidence->{tstEvidence}) {
394 4 50       6 foreach my $ts_and_crl (@{ $evidence->{tstEvidence} || [] }) {
  4         18  
395 4         9 my $ci_struct = $ts_and_crl->{timeStamp};
396 4 50       32 next unless defined $ci_struct;
397 4         20 my $der = $CONTENTINFO_CODEC->encode($ci_struct);
398 4 50       840 push @tokens_der, $der if defined $der;
399             }
400             }
401 4         17 return \@tokens_der;
402             }
403              
404             # Convenience helpers to write extracted payloads
405             sub write_content_file {
406 1     1 1 1488 my ($class, $tsd_hashref, $filepath) = @_;
407 1         4 my $bytes = $class->extract_content_der($tsd_hashref);
408 1 50       4 die 'No embedded content found in TSD' unless defined $bytes;
409 1 50       196 open my $fh, '>:raw', $filepath or die "Cannot open '$filepath' for writing: $!";
410 1         3 print {$fh} $bytes;
  1         14  
411 1         32 close $fh;
412 1         9 return 1;
413             }
414              
415             ## Extract encapsulated content from a SignedData (p7m) stored in TSD.content.
416             ## Returns raw bytes of the signed payload (eContent) when available.
417             sub extract_signed_content_bytes {
418 0     0 1 0 my ($class, $tsd_hashref) = @_;
419 0         0 my $content_der = $tsd_hashref->{content};
420 0 0       0 return undef unless defined $content_der;
421              
422 0 0       0 my $ci = $CONTENTINFO_CODEC->decode($content_der) or return undef;
423 0 0 0     0 return undef unless defined $ci->{contentType} && $ci->{contentType} eq $OID_CT_SIGNEDDATA;
424 0         0 my $sd_der = $ci->{content};
425 0 0       0 my $sd = $SIGNEDDATA_CODEC->decode($sd_der) or return undef;
426 0   0     0 my $eci = $sd->{encapContentInfo} || {};
427 0         0 my $econtent = $eci->{eContent};
428 0 0       0 return undef unless defined $econtent;
429             # eContent is EXPLICIT OCTET STRING; unwrap if needed
430 0 0       0 my $first_tag = length($econtent) ? ord(substr($econtent, 0, 1)) : -1;
431 0 0       0 if ($first_tag == 0x04) {
432 0         0 my $octets = $OCTETSTRING_CODEC->decode($econtent);
433 0 0       0 return $octets if defined $octets;
434             }
435 0         0 return $econtent;
436             }
437              
438             sub write_signed_content_file {
439 0     0 1 0 my ($class, $tsd_hashref, $filepath) = @_;
440 0         0 my $bytes = $class->extract_signed_content_bytes($tsd_hashref);
441 0 0       0 die 'No encapsulated signed content found in TSD.content' unless defined $bytes;
442 0 0       0 open my $fh, '>:raw', $filepath or die "Cannot open '$filepath' for writing: $!";
443 0         0 print {$fh} $bytes;
  0         0  
444 0         0 close $fh;
445 0         0 return 1;
446             }
447              
448             sub write_tst_files {
449 2     2 1 1140 my ($class, $tsd_hashref, $dirpath) = @_;
450 2         9 my $tokens = $class->extract_tst_tokens_der($tsd_hashref);
451 2         4 my $index = 1;
452 2         6 foreach my $der (@$tokens) {
453 2         7 my $out = "$dirpath/timestamp_$index.tsr"; # DER CMS ContentInfo
454 2 50       288 open my $fh, '>:raw', $out or die "Cannot open '$out' for writing: $!";
455 2         19 print {$fh} $der;
  2         37  
456 2         142 close $fh;
457 2         16 $index++;
458             }
459 2         21 return scalar(@$tokens);
460             }
461              
462             ###
463             # Create and write a TSD file from a marked file and one or more RFC3161
464             # TimeStampToken(s) provided as .TSR (DER CMS ContentInfo) blobs or paths.
465             #
466             # Usage examples:
467             # # single token, infer output path as .tsd
468             # my $out = Crypt::TimestampedData->write_tds('/path/file.bin', '/path/token.tsr');
469             #
470             # # multiple tokens
471             # my $out = Crypt::TimestampedData->write_tds('/path/file.bin', [ '/p/a.tsr', '/p/b.tsr' ], '/path/out.tsd');
472             #
473             # Args:
474             # $marked_filepath - path to the original file to embed
475             # $tsr_input - path to .tsr, raw DER bytes, or ARRAYREF of these
476             # $out_filepath_opt - optional output .tsd path; defaults to "$marked_filepath" with .tsd extension
477             #
478             # Returns output filepath on success.
479             sub write_tds {
480 4     4 1 639115 my ($class, $marked_filepath, $tsr_input, $out_filepath_opt) = @_;
481              
482 4 50       17 die 'Marked file path is required' unless defined $marked_filepath;
483 4 50       207 open my $in_fh, '<:raw', $marked_filepath or die "Cannot open marked file '$marked_filepath' for reading: $!";
484 4         26 local $/; my $marked_bytes = <$in_fh>; close $in_fh;
  4         100  
  4         44  
485              
486             # If the marked bytes are already a CMS ContentInfo (e.g., .p7m), embed as-is.
487             # Otherwise, build a ContentInfo (id-data) wrapping the raw bytes.
488 4         10 my $contentinfo_der;
489 4         38 my $ci_probe = $CONTENTINFO_CODEC->decode($marked_bytes);
490 4 50 33     550 if (defined $ci_probe && ref $ci_probe eq 'HASH' && exists $ci_probe->{contentType}) {
      33        
491 0         0 $contentinfo_der = $marked_bytes; # already a ContentInfo
492             } else {
493 4         11 my $DATA_OID = '1.2.840.113549.1.7.1'; # id-data
494 4 50       21 my $octets_der = $OCTETSTRING_CODEC->encode($marked_bytes)
495             or die 'ASN.1 encode failed for OctetString: ' . $OCTETSTRING_CODEC->error;
496 4 50       356 $contentinfo_der = $CONTENTINFO_CODEC->encode({ contentType => $DATA_OID, content => $octets_der })
497             or die 'ASN.1 encode failed for ContentInfo (data): ' . $CONTENTINFO_CODEC->error;
498             }
499              
500             # Normalize $tsr_input to an array of DER-encoded TimeStampToken ContentInfo blobs
501 4 50       841 my @tsr_items = ref($tsr_input) eq 'ARRAY' ? @$tsr_input : ($tsr_input);
502 4 50       42 die 'At least one .TSR token must be provided' unless @tsr_items;
503              
504 4         9 my @tst_seq;
505 4         19 for my $item (@tsr_items) {
506 4         8 my $der;
507 4 50 33     175 if (defined $item && !ref($item) && -e $item) {
      33        
508 4 50       186 open my $fh, '<:raw', $item or die "Cannot open TSR file '$item' for reading: $!";
509 4         22 local $/; $der = <$fh>; close $fh;
  4         175  
  4         71  
510             } else {
511 0         0 $der = $item;
512             }
513 4 50 33     29 die 'Undefined TSR DER provided' unless defined $der && length $der;
514 4 50       25 my $ci_struct = $CONTENTINFO_CODEC->decode($der)
515             or die 'ASN.1 decode failed for TSR (expecting ContentInfo/TimeStampToken): ' . $CONTENTINFO_CODEC->error;
516 4         1216 push @tst_seq, { timeStamp => $ci_struct };
517             }
518              
519 4         54 my ($fname) = $marked_filepath =~ m{([^/]+)$};
520 4 50       55 my $tsd_hashref = {
521             version => 1,
522             metaData => {
523             hashProtected => 0,
524             (defined $fname ? (fileName => $fname) : ()),
525             },
526             content => $contentinfo_der,
527             temporalEvidence => {
528             tstEvidence => \@tst_seq,
529             },
530             };
531              
532             # Determine output path
533 4         15 my $out = $out_filepath_opt;
534 4 50 33     26 unless (defined $out && length $out) {
535 0         0 $out = $marked_filepath;
536 0         0 $out =~ s{(\.[^/\.]+)?$}{.tsd};
537             }
538              
539             # For better interoperability, wrap TSD as CMS ContentInfo with id-ct-TSTData
540 4 50       17 my $tsd_der = $TSD_CODEC->encode($tsd_hashref)
541             or die 'ASN.1 encode failed for TimeStampedData: ' . $TSD_CODEC->error;
542 4 50       2350 my $ci_tsd_der = $CONTENTINFO_CODEC->encode({ contentType => $OID_CT_TSTDATA, content => $tsd_der })
543             or die 'ASN.1 encode failed for ContentInfo(id-ct-TSTData): ' . $CONTENTINFO_CODEC->error;
544 4 50       1440 open my $out_fh, '>:raw', $out or die "Cannot open TSD file '$out' for writing: $!";
545 4         15 print {$out_fh} $ci_tsd_der;
  4         80  
546 4         224 close $out_fh;
547 4         117 return $out;
548             }
549              
550             1;