Crypt-TimestampedData

 view release on metacpan or  search on metacpan

lib/Crypt/TimestampedData.pm  view on Meta::CPAN

package Crypt::TimestampedData;
$Crypt::TimestampedData::VERSION = '0.02';
use strict;
use warnings;

use Convert::ASN1;

=pod

=head1 NAME

Crypt::TimestampedData - Read and write TimeStampedData files (.TSD, RFC 5544)

=head1 VERSION

version 0.02

=head1 SYNOPSIS

  use Crypt::TimestampedData;

  # Decode from .TSD file
  my $tsd = Crypt::TimestampedData->read_file('/path/file.tsd');
  my $version           = $tsd->{version};
  my $data_uri          = $tsd->{dataUri};        # optional
  my $meta              = $tsd->{metaData};       # optional
  my $content_der       = $tsd->{content};        # optional (CMS ContentInfo DER)
  my $evidence_content  = $tsd->{temporalEvidence};

  # Encode to .TSD file
  Crypt::TimestampedData->write_file('/path/out.tsd', $tsd);

=head1 DESCRIPTION

Minimal implementation of the TimeStampedData format (RFC 5544) using Convert::ASN1.
This version treats CMS constructs and TimeStampTokens as opaque DER blobs.
The goal is to enable reading/writing of .TSD files, delegating CMS/TS handling
to external libraries when available.

=head1 SECURITY

Report security vulnerabilities **privately** to the maintainer at
E<lt>gdo@leader.itE<gt>. See the F<SECURITY.md> file in this distribution's
root directory for the full policy (coordinated disclosure, optional CC to the
L<CPAN Security Group|https://security.metacpan.org/> at
E<lt>cpan-security@security.metacpan.orgE<gt>). Do not file security issues on
public bug trackers before coordination.

=head1 METHODS

=head2 new(%args)

Creates a new Crypt::TimestampedData object with the provided arguments.

=head2 read_file($filepath)

Reads and decodes a .TSD file from the specified path. Returns a hash reference
containing the decoded TimeStampedData structure.

=head2 write_file($filepath, $tsd_hashref)

Encodes and writes a TimeStampedData structure to the specified file path.

=head2 decode_der($der)

Decodes DER-encoded TimeStampedData. Handles both direct TSD format and
CMS ContentInfo wrappers (id-ct-TSTData and pkcs7-signedData).

=head2 encode_der($tsd_hashref)

Encodes a TimeStampedData hash reference to DER format.

=head2 extract_content_der($tsd_hashref)

Extracts the embedded original content from TimeStampedData.content.
Returns raw bytes of the original file if available, otherwise undef.

=head2 extract_tst_tokens_der($tsd_hashref)

Extracts RFC 3161 TimeStampToken(s) as DER ContentInfo blobs.
Returns array reference of DER-encoded ContentInfo tokens.

=head2 write_content_file($tsd_hashref, $filepath)

Convenience method to write extracted content to a file.

=head2 extract_signed_content_bytes($tsd_hashref)

Extracts encapsulated content from a SignedData (p7m) stored in TSD.content.
Returns raw bytes of the signed payload (eContent) when available.

=head2 write_signed_content_file($tsd_hashref, $filepath)

Convenience method to write extracted signed content to a file.

=head2 write_tst_files($tsd_hashref, $dirpath)

Writes extracted timestamp tokens to individual .tsr files in the specified directory.

=head2 write_tds($marked_filepath, $tsr_input, $out_filepath_opt)

Creates and writes a TSD file from a marked file and one or more RFC3161
TimeStampToken(s) provided as .TSR (DER CMS ContentInfo) blobs or paths.

=head1 EXAMPLES

  # Read a TSD file
  my $tsd = Crypt::TimestampedData->read_file('document.tsd');
  print "Version: $tsd->{version}\n";
  
  # Extract the original content
  Crypt::TimestampedData->write_content_file($tsd, 'original_document.pdf');
  
  # Extract timestamp tokens
  my $tokens = Crypt::TimestampedData->extract_tst_tokens_der($tsd);
  print "Found " . scalar(@$tokens) . " timestamp tokens\n";
  
  # Create a new TSD file

lib/Crypt/TimestampedData.pm  view on Meta::CPAN

    content       [0] EXPLICIT ANY OPTIONAL
  }


  -- Helper to unwrap OCTET STRING containers when needed
  OctetString ::= OCTET STRING

  -- Minimal CMS structures to navigate SignedData → EncapsulatedContentInfo
  SignedData ::= SEQUENCE {
    version                INTEGER,
    digestAlgorithms       SET OF AlgorithmIdentifier,
    encapContentInfo       EncapsulatedContentInfo,
    certificates           [0] IMPLICIT ANY OPTIONAL,
    crls                   [1] IMPLICIT ANY OPTIONAL,
    signerInfos            SET OF ANY
  }

  EncapsulatedContentInfo ::= SEQUENCE {
    eContentType           OBJECT IDENTIFIER,
    eContent               [0] EXPLICIT OCTET STRING OPTIONAL
  }

  AlgorithmIdentifier ::= SEQUENCE {
    algorithm              OBJECT IDENTIFIER,
    parameters             ANY OPTIONAL
  }

__ASN1_RFC5544__

my $ASN1 = Convert::ASN1->new;
$ASN1->prepare($ASN1_SPEC) or die "TSD ASN.1 prepare error: " . $ASN1->error;

my $TSD_CODEC = $ASN1->find('TimeStampedData')
  or die 'ASN.1 type TimeStampedData not found';

my $CONTENTINFO_CODEC = $ASN1->find('ContentInfo')
  or die 'ASN.1 type ContentInfo not found';

my $OCTETSTRING_CODEC = $ASN1->find('OctetString')
  or die 'ASN.1 type OctetString not found';

my $SIGNEDDATA_CODEC = $ASN1->find('SignedData')
  or die 'ASN.1 type SignedData not found';

my $CONTENTINFO_TSD_CODEC = $ASN1->find('ContentInfoTSD')
  or undef;

my $OID_CT_TSTDATA = '1.2.840.113549.1.9.16.1.31';
my $OID_CT_SIGNEDDATA = '1.2.840.113549.1.7.2';

sub new {
  my ($class, %args) = @_;
  my $self = {%args};
  return bless $self, $class;
}

sub decode_der {
  my ($class, $der) = @_;

  # Try direct TimeStampedData first
  my $decoded = $TSD_CODEC->decode($der);
  return $decoded if defined $decoded;

  # If that fails, try unwrapping CMS ContentInfo (common packaging for TSD)
  my $ci = $CONTENTINFO_CODEC->decode($der);
  die 'ASN.1 decode failed: ' . $TSD_CODEC->error unless defined $ci;

  # Two acceptable wrappings for RFC 5544:
  # 1) ContentInfo with contentType id-ct-TSTData and [0] content = TimeStampedData
  # 2) ContentInfo with contentType pkcs7-signedData, whose encapContentInfo.eContentType is id-ct-TSTData

  my $content_der = $ci->{content};
  die 'ASN.1 decode failed: ContentInfo without [0] content' unless defined $content_der;

  # Some encoders place TSD directly as [0] EXPLICIT SEQUENCE; others wrap as OCTET STRING
  my $first_tag = length($content_der) ? ord(substr($content_der, 0, 1)) : -1;
  if ($first_tag == 0x04) { # OCTET STRING
    my $octets = $OCTETSTRING_CODEC->decode($content_der);
    die 'ASN.1 decode failed: cannot unwrap OCTET STRING: ' . $OCTETSTRING_CODEC->error unless defined $octets;
    $content_der = $octets;
  }

  # Case 1: Direct id-ct-TSTData wrapper — decode TSD from [0] content (may be OCTET STRING)
  if (defined $ci->{contentType} && $ci->{contentType} eq $OID_CT_TSTDATA) {
    my $tsd = $TSD_CODEC->decode($content_der);
    die 'ASN.1 decode failed (inside ContentInfo/id-ct-TSTData): ' . $TSD_CODEC->error unless defined $tsd;
    return $tsd;
  }

  # Case 2: SignedData → EncapsulatedContentInfo with id-ct-TSTData
  if (defined $ci->{contentType} && $ci->{contentType} eq $OID_CT_SIGNEDDATA) {
    my $sd = $SIGNEDDATA_CODEC->decode($content_der);
    die 'ASN.1 decode failed: cannot decode SignedData: ' . $SIGNEDDATA_CODEC->error unless defined $sd;
    my $eci = $sd->{encapContentInfo} || {};
    my $econtent_type = $eci->{eContentType};
    my $econtent = $eci->{eContent};
    die 'ASN.1 decode failed: SignedData without encapContentInfo.eContent' unless defined $econtent;
    unless (defined $econtent_type && $econtent_type eq $OID_CT_TSTDATA) {
      die "Input is CMS SignedData with eContentType '$econtent_type', expected id-ct-TSTData ($OID_CT_TSTDATA).";
    }
    # eContent is an OCTET STRING wrapping the TSD DER
    my $tsd = $TSD_CODEC->decode($econtent);
    die 'ASN.1 decode failed (inside SignedData/id-ct-TSTData): ' . $TSD_CODEC->error unless defined $tsd;
    return $tsd;
  }

  die "Input ContentInfo has unsupported contentType '$ci->{contentType}', expected id-ct-TSTData ($OID_CT_TSTDATA) or pkcs7-signedData ($OID_CT_SIGNEDDATA).";
}

sub encode_der {
  my ($class, $tsd_hashref) = @_;
  my $der = $TSD_CODEC->encode($tsd_hashref);
  die 'ASN.1 encode failed: ' . $TSD_CODEC->error unless defined $der;
  return $der;
}

sub read_file {
  my ($class, $filepath) = @_;
  open my $fh, '<:raw', $filepath or die "Cannot open TSD file '$filepath' for reading: $!";
  local $/; my $der = <$fh>; close $fh;
  return $class->decode_der($der);
}



( run in 2.464 seconds using v1.01-cache-2.11-cpan-df04353d9ac )