package ELF::sign;
use strict;
use Fcntl qw(SEEK_END);
use Digest::SHA qw(sha512);
use vars qw($VERSION @ISA);
$VERSION = '0.05';
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.05
=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}()>
and does do not any further certificate, ca processing or 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
optional 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