package ELF::sign;

use strict;
use Net::SSLeay;
use Fcntl qw(SEEK_END);
use Digest::SHA qw(sha512);

use vars qw($VERSION @ISA);
$VERSION = '0.04';

BEGIN {
   eval {
      require Net::SSLeay;
      Net::SSLeay->import( 1.65 );
   };
   Net::SSLeay::load_error_strings();
   Net::SSLeay::SSLeay_add_ssl_algorithms();
   Net::SSLeay::randomize();
}

require XSLoader;
XSLoader::load('ELF::sign', $VERSION);

sub new {
   my $type = shift;
   my $params = {@_};
   my $self = bless({}, $type);
   $self->{debug}++;
   return $self;
}

sub dataFile {
   my $self = shift;
   my $file = shift;
   $self->{file} = $file;
   $self->{pkcs7} = $self->getFromFile();
   $self->{datapostset} = $self->{pkcs7} ? length($self->{pkcs7}) + 8 : 0;
   return $self->{pkcs7};
}

sub data {
   my $self = shift;
   my $data = shift;
   delete $self->{file};
   $self->{data} = $data;
   $self->{pkcs7} = $self->getFromData();
   $self->{datapostset} = $self->{pkcs7} ? length($self->{pkcs7}) + 8 : 0;
   return $self->{pkcs7};
}

sub crt {
   shift->{"crt"} = shift;
}

sub key {
   shift->{"key"} = shift;
}

sub crtFile {
   return shift->loadFile("crt", @_);
}

sub keyFile {
   return shift->loadFile("key", @_);
}

sub load {
   my $self = shift;
   my $name = shift;
   my $data = shift;
   $self->{$name} = $data;
   return undef;
}

sub loadFile {
   my $self = shift;
   my $name = shift;
   my $file = shift;
   delete $self->{$name};
   open(IN, "<", $file) ||
      return "Error opening ".$name.": ".$!;
   while(<IN>) {
      $self->{$name} .= $_;
   }
   close(IN);
   return undef;
}

sub dataToBio {
   my $data = shift;
   #my $self = {};
   my $bio = Net::SSLeay::BIO_new(Net::SSLeay::BIO_s_mem());
   my $sent = Net::SSLeay::BIO_write($bio, $data);
   #print "Wrote ".$sent." of ".length($data)." bytes.\n"
   #   if $self->{debug};
   die "Cannot write to bio!"
      if (($sent) != length($data));
   return $bio;
}

sub PEMdataToPKCS7 {
   my $data = shift;
   my $pkcs7 = undef;
   my $bio = dataToBio($data);
   die "Error using pkcs7: ".Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error())
      unless ($pkcs7 = PEM_read_bio_PKCS7($bio, 0, 0, 0));
   Net::SSLeay::BIO_free($bio);
   return $pkcs7;
}

sub PEMdataToX509 {
   my $x509 = shift;
   my $bio = dataToBio($x509);
   my $x509result = undef;
   die "Error using x509: ".Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error())
      unless ($x509result = Net::SSLeay::PEM_read_bio_X509($bio));
   Net::SSLeay::BIO_free($bio);
   return $x509result;
}

sub PEMdataToEVP_PKEY {
   my $crt = shift;
   my $bio = dataToBio($crt);
   my $evp_pkey = undef;
   die "Error using cacrt: ".Net::SSLeay::ERR_error_string(Net::SSLeay::ERR_get_error())
      unless ($evp_pkey = Net::SSLeay::PEM_read_bio_PrivateKey($bio));
   Net::SSLeay::BIO_free($bio);
   return $evp_pkey;
}

sub getDigest {
   my $self = shift;
   my $sha512 = Digest::SHA->new("sha512");
   if ($self->{file}) {
      my $filesize = doFile(sub{
         my $buf = shift;
         $sha512->add($buf);
      }, $self->{file}, $self->{datapostset});
      return $filesize
         unless $filesize;
   } elsif(defined($self->{data})) {
      $sha512->add(substr($self->{data}, 0, length($self->{data})-$self->{datapostset}));
   } else {
      return undef;
   }
   return $sha512->digest();
}

sub sign {
   my $self = shift;
   # TODO:XXX:FIXME: Allow to specify which digest should be used!
   # TODO:XXX:FIXME: Allow to specify ca the certificate must match!
   # TODO:XXX:FIXME: Verify the validity of the certificate used!
   delete $self->{pkcs7};
   my $digest = $self->getDigest();
   return "No digest"
      unless $digest;
   return "No Cert"
      unless $self->{crt};
   return "No Key"
      unless $self->{key};
   $self->{pkcs7} = datasign($digest, PEMdataToX509($self->{crt}), PEMdataToEVP_PKEY($self->{key}));
   return $self->{pkcs7} ? undef : "Signing failed";
}

sub doFile {
  my $action = shift;
  my $file = shift;
  my $lenp7 = shift;
  return undef
     unless my $size = (stat($file))[7];
  open(IN, "<", $file) || die($!);
  my $filesize = 0;
  while (1) {
      my $wread = 16*1024;
      if ($lenp7 && ($wread+$filesize > ($size-$lenp7))) {
         last unless
            $wread = $size-($filesize+$lenp7) > 0;
         #print("Reducing wread=16k -> ".$wread." size=".$size." lenp7=".$lenp7." pos=".$filesize." file=".$file."\n");
      }
      if ((my $nread = sysread(IN, my $buf, $wread)) > 0) {
         &$action($buf);
         $filesize += $nread;
      } else {
         last;
      }
  }
  close(IN);
  return $filesize;
}

sub verify {
   my $self = shift;
   # TODO:XXX:FIXME: Allow to specify which digest should be used!
   # TODO:XXX:FIXME: Allow to specify ca the certificate must match!
   # TODO:XXX:FIXME: Verify the validity of the certificate used!
   my $digest = $self->getDigest();
   return "No digest"
      unless $digest;
   return "No PKCS7"
      unless $self->{pkcs7};
   return "No Cert"
      unless $self->{crt};
   my $return = dataverify($digest, PEMdataToX509($self->{crt}), PEMdataToPKCS7($self->{pkcs7}));
   return $return;
}

sub hexdump { join ':', map { sprintf "%02X", $_ } unpack "C*", $_[0]; }

sub save {
   my $self = shift;
   my $newfile = shift;
   my $nopkcs7 = shift || 0;
   return "Unsigned"
      unless $self->{pkcs7} || $nopkcs7;
   open(my $outfile, ">", $newfile) ||
      return "Cannot open file: ".$!;
   if ($self->{file}) {
      my $filesize = doFile(sub{
         my $buf = shift;
         syswrite($outfile, $buf);
      }, $self->{file}, $self->{datapostset});
      return "Error reading file"
         unless defined($filesize);
   } elsif(defined($self->{data})) {
      syswrite($outfile, substr($self->{data}, 0, length($self->{data})-$self->{datapostset}));
   } else {
      return "No data";
   }
   syswrite($outfile, $self->{pkcs7}.pack("Q", length($self->{pkcs7})))
      unless $nopkcs7;
   close($outfile);
   return undef;
}

sub get {
   my $self = shift;
   my $nopkcs7 = shift || 0;
   return undef
      unless $self->{pkcs7} || $nopkcs7;
   my $data = '';
   if ($self->{file}) {
      my $filesize = doFile(sub{
         my $buf = shift;
         $data .= $buf;
      }, $self->{file}, $self->{datapostset});
   } elsif(defined($self->{data})) {
      $data = substr($self->{data}, 0, length($self->{data})-$self->{datapostset});
   } else {
      return undef;
   }
   $data .= $self->{pkcs7}.pack("Q", length($self->{pkcs7}))
      unless $nopkcs7;
   return $data;
}

sub getFromData {
   my $self = shift;
   if (length($self->{data}) < 8) {
      #print "Unable to access 8 bytes! ".length($self->{data})."\n";
      return 0;
   }
   my $offset = unpack("Q", substr($self->{data}, length($self->{data})-8, 8));
   if ($offset+8 > length($self->{data})) {
      #print "Offset bigger than data -> Skipping\n";
      return 0;
   }
   if ($offset > 10*1024) {
      #print "Offset bigger than one MB -> Skipping\n";
      return 0;
   }
   my $wanted = "-----BEGIN PKCS7-----";
   if (substr($self->{data}, length($self->{data})-(8+$offset), length($wanted)) ne $wanted) {
      #print "Not a PKCS7 header!\n";
      return 0;
   }
   return substr($self->{data}, length($self->{data})-(8+$offset), $offset);
}

sub getFromFile {
   my $self = shift;
   unless ($self->{file}) {
      #print "Unable to read 8 bytes!\n";
      return undef;
   }
   my $filesize = (stat($self->{file}))[7];
   my $openfile = undef;
   unless (open($openfile, "<", $self->{file})) {
      #print "Unable to open file="($self->{file}." error=".$!."\n";
      return undef;
   }
   seek($openfile, -8, SEEK_END);
   my $data = undef;
   unless ((sysread($openfile, $data, 8)) == 8) {
      #print "Unable to read 8 bytes!\n";
      return 0;
   }
   my $offset = unpack("Q", $data);
   if ($offset+8 > $filesize) {
      #print "Offset bigger than file -> Skipping\n";
      return 0;
   }
   if ($offset > 1024*1024) {
      #print "Offset bigger than one MB -> Skipping\n";
      return 0;
   }
   #print "Found offset of ".$offset."\n";
   seek($openfile, -(8+$offset), SEEK_END);
   my $wanted = "-----BEGIN PKCS7-----";
   unless ((sysread($openfile, $data, length($wanted))) == length($wanted)) {
      #print "Unable to read ".length($wanted)." bytes!\n";
      return 0;
   }
   if ($data ne $wanted) {
      #print "Not a PKCS7 header!";
      return 0;
   }
   seek($openfile, -(8+$offset), SEEK_END);
   unless ((sysread($openfile, $data, $offset)) == $offset) {
      #print "Unable to read $offset bytes!\n";
      return 0;
   }
   return $data;
}

sub pkcs7 {
   my $self = shift;
   my $pkcs7 = shift;
   $self->{pkcs7} = $pkcs7
      if defined($pkcs7);
   return $self->{pkcs7};
}

1;

__END__

=head1 NAME

ELF::sign - X509 signing of elf execuables

=head1 VERSION

Version 0.04

=over 2

=back

=head1 DESCRIPTION

This module allows one to sign a elf file - or any other file type - based
on a PKCS#7 via a X509-Certifcate and its private key, and include the
signature in the file.

It uses SHA512 Hashing via PKCS#7 to ensure the correctness.

=over 2

=back

=head1 SYNOPSIS

=head2 Signing - File or inmemory based

You can mix inmemory and file based commands.

=over 2

=item File based

  use ELF::sign;
  my $sign = ELF::sign->new();
  $sign->dataFile($file);
  $x->crtFile("test.crt");
  $x->keyFile("test.key");
  if (my $error = $x->sign()) {
     print "  ERROR: sign: ".$error."\n";
  } else {
     print "  Signed.\n";
  }
  if (my $error = $x->save($outfile)) {
     print "  ERROR: signFileInmemory->save: ".$error."\n";
  }

=back

=over 2

=item In-Memory based

   use ELF::sign;
   my $sign = ELF::sign->new();
   $sign->data(time());
   $x->crt("-----BEGIN CERTIFICATE-----
            ...
            -----END CERTIFICATE-----");
   $x->key("...");
   if (my $error = $x->sign()) {
      print "ERROR: sign: ".$error."\n";
   } else {
      print "Signed.\n";
   }
   my $signeddata = $x->get());

=back

=head2 Verifying - File or inmemory based

You can mix inmemory and file based commands.

=over 2

=item File based

  use ELF::sign;
  my $sign = ELF::sign->new();
  $sign->dataFile($file);
  $x->crtFile("test.crt");
  if (my $error = $x->verify()) {
     print "ERROR: verify: ".$error."\n";
     return;
  } else {
     print "Verified.\n";
  }
  if (my $error = $x->save($outfile, 1)) {
     print "ERROR: save: ".$error."\n";
  }

=back

=over 2

=item In-Memory based

   use ELF::sign;
   my $sign = ELF::sign->new();
   $sign->data(time());
   $x->crt("-----BEGIN CERTIFICATE-----
            ...
            -----END CERTIFICATE-----");
   if (my $error = $x->verify()) {
      print "ERROR: verify: ".$error."\n";
   } else {
      print "Verified.\n";
   }
   my $signeddata = $x->get(1));

=back

=head1 FUNCTIONS

=over 2

=item new

Returns a new I<ELF::sign> object. It ignores any options.

=item data{File}($data{/$filename})

Assignes data (maybe as a file) on which signing or verifying operations can be
applied.

Detects automatically if there is already a signature on the file or on the data,
and parses it in this case. Verifying is only possible if there is one or if
I<sign()> has been successfully called. Signing is possible in any case, and
overwrites a maybe exsting signing.

If I<data{File}()> is used, you specify a file. If this file cannot be read it
returns undef.

In any other case, also on I<data()>, it returns the the attached signing (PKCS#7)
or the the value 0 if there is none.

=item crt{File}($data{/$filename})

Assignes a X509-certificate to be used for verifing or signing. To sign you also
need to set the corresponding I<key{File}()>.

=item key{File}($data{/$filename})

Assignes a key to be used for signing via I<sign()>. To sign you also need to set the
corresponding I<crt{File}()>.

=item verify()

Verifies that a attached or via I<sign()> created signature matches the data passed
via I<data{File}()> and the public key of I<crt{File}()>.

Returns undef on success, or on any error the cause as scalar(string).

B<WARNING:> ELF::sign currently does not verify the validity of the certificate,
            it only uses the public key in the certificate specified by I<crt{File}()>
            matches the signature of the hash of the data and does do not any further
            certificate processing or ca checks.
            This will get fixed in one of the next releases.

=item sign()

Creates inmemory a PKCS#7 signature via I<crt{File}()> and I<key{File}()> on the
data that has been passed via I<data{File}()>. Returns undef on success, or on
any error the cause as scalar(string).

To store and attach this signature you have to use I<get()> or I<save()>. The
signature alone, the PKCS#7, can be fetched via I<pkcs7()>.

=item get({1})

Returns the passed data passed via I<data{File}()> as scalar(string), and the
attached signature, if available. If the optional parameter is true, it omits
the signature.

=item save($filename{,1})

Saves the passed data passed via I<data{File}()> to a file, including the attached
signature if available. If the optional parameter is true, it omits the signature.

=item pkcs7({$data})

Returns the currently active PKCS#7 signature, if available, or undef. Via the
optinal data the pkcs7 can be manually overwritten.

=item hexdump($string)

Returns string data in hex format.

Example:

  perl -e 'use ELF::sign; print ELF::sign->hexdump("test")."\n";'
  74:65:73:74

=back

=head2 Internal functions

=over 2

=item crt()

=item crtFile()

=item key()

=item keyFile()

=item data()

=item dataFile()

=item datasign()

=item dataverify()

=item load()

=item loadFile()

=item dataToBio()

=item PEMdataToPKCS7()

=item PEMdataToX509()

=item PEMdataToEVP_PKEY()

=item getDigest()

=item doFile()

=item getFromData()

=item getFromFile()

=item PEM_read_bio_PKCS7()

=back

=head1 Commercial support

Commercial support can be gained at <elfsignsupport at cryptomagic.eu>.

Used in our products, you can find on L<https://www.cryptomagic.eu/>

=head1 COPYRIGHT & LICENSE

Copyright 2010-2018 Markus Schraeder, CryptoMagic GmbH, all rights reserved.

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=cut

1; # End of ELF::sign
