To sign a pdf document you need to build some pdf dictionary objects and rebuild the document to include them. Page 12 in Acrobat_DigitalSignatures_in_PDF.pdf shows the steps involved. I found a Stackoverflow thread that has a link to an example that looks promising but I haven't tried it.
Edit: Adding the example from the link above.
#! env perl
#
# Copyright (c) 2012, Martin Schuette <info@mschuette.name>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions a
+re met:
#
# 1. Redistributions of source code must retain the above copyright no
+tice, this
# list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
+ notice,
# this list of conditions and the following disclaimer in the docum
+entation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO
+, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR P
+URPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS B
+E LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQU
+ENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GO
+ODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) H
+OWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT L
+IABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT O
+F THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use v5.12;
use strict;
use warnings;
use PDF::API2;
use PDF::API2::Basic::PDF::Utils;
use File::Slurp;
use Time::Piece;
#use Data::Dumper;
use Crypt::OpenSSL::RSA;
use Crypt::OpenSSL::X509 qw/FORMAT_UNDEF FORMAT_ASN1 FORMAT_TEXT FORMA
+T_PEM/;
use Digest::SHA1 qw(sha1 sha1_hex);
my $add_mdp = 0; # not tested; add DocMDP-signature attribu
+tes
my $input_filename = "test.pdf";
my $tempfilename = '/tmp/tmp.pdf';
my $outfilename = '/tmp/test.pdf';
# 'pkcs7' is the preferred way, inserts a detached PKCS#7 signature
# the (untested) alternative is 'rsa' which adds a PKCS#1 of SHA-1
my $sig_algorithm = 'pkcs7';
my $sig_length = 20480;
# certificates:
my $cacert_filename = "mschuetteca.crt";
my $x509_filename = "mschuett.pem";
my $priv_key_filename = "mschuett.pem";
my $cacert = Crypt::OpenSSL::X509->new_from_file($cacert_filename);
my $x509 = Crypt::OpenSSL::X509->new_from_file($x509_filename);
my $priv_key = read_file($priv_key_filename);
# prepare different values for the signature meta-data
sub pdf_timestamp {
my $timestamp = localtime;
my $tz = $timestamp->strftime("%z");
$tz =~ s/([\+\-])(\d\d)(\d\d)/$1$2'$3'/;
my $timestring = $timestamp->strftime("D:%Y%m%d%H%M%S$tz");
return PDFStr $timestring;
}
sub pdf_location {
my $location = `hostname`;
chomp $location;
return PDFStr $location;
}
sub pdf_contactinfo {
return PDFStr($x509->email());
}
sub pdf_signername {
return PDFStr($x509->subject_name->as_string);
}
# Basic structure: we have to insert 4 dictionaries into the PDF:
#
# 1. an AcroForm dictionary, which has an Array of Form elements.
# Here only one reference to our Field dictionary.
# 2. the Field dictionary (with /FT/Sig)
# 3. the Signature dictionary (with /Type/Sig) containing the
# "signature itself" in its /Contents
# 4. the Annotation dictionary (with /Type/Annot /Subtype/Widget)
# to link the signature to a page and possibly to a graphic
my $pdf = PDF::API2->open($input_filename);
my $p = $pdf->{catalog}->{' parent'};
# create Signature dictionary (with /Type/Sig)
my $sigdict = PDF::API2::Basic::PDF::Dict->new();
$sigdict->{Type} = PDFName("Sig");
$sigdict->{Filter} = PDFName("Adobe.PPKLite");
$sigdict->{Reason} = PDFStr("Testing my PDF Signature Demo Tool");
$sigdict->{Name} = pdf_signername();
$sigdict->{ContactInfo} = pdf_contactinfo();
$sigdict->{Location} = pdf_location();
$sigdict->{M} = pdf_timestamp();
# algorithm/encoding dependent fields and values:
if ($sig_algorithm eq 'rsa') {
$sigdict->{SubFilter} = PDFName('adbe.x509.rsa.sha1');
my @certs;
push @certs, PDFStr $x509->as_string(FORMAT_ASN1);
push @certs, PDFStr $cacert->as_string(FORMAT_ASN1);
# only for PCKS#1:
$sigdict->{Cert} = PDFArray @certs if ($sig_algorithm eq 'rsa')
+;
} else {
$sigdict->{SubFilter} = PDFName('adbe.pkcs7.detached');
}
# placeholder:
$sigdict->{Contents} = PDFStrHex("\0" x $sig_length);
$sigdict->{ByteRange} = PDF::API2::Basic::PDF::Literal->new("[0 000000
+00 00000000 00000000]");
if ($add_mdp) {
# for DocMDP signatures we insert a secondary dict with more info
my $sigrefdict = PDF::API2::Basic::PDF::Dict->new();
$sigrefdict->{Type} = PDFName("SigRef");
$sigrefdict->{TransformMethod} = PDFName("DocMDP");
$sigrefdict = $p->new_obj($sigrefdict);
$sigdict->{Reference} = PDFArray($sigrefdict);
}
# finalize object:
$sigdict = $p->new_obj($sigdict);
# the Field dictionary gets an Annotation Widget as a child element
my $sigannotdict = PDF::API2::Basic::PDF::Dict->new();
# Field dictionary (with /FT/Sig)
my $sigformdict = PDF::API2::Basic::PDF::Dict->new();
$sigformdict->{FT} = PDFName("Sig");
$sigformdict->{T} = PDFStr("Demo Signature");
$sigformdict->{V} = $sigdict;
$sigformdict->{Kids} = PDFArray($sigannotdict);
$sigformdict = $p->new_obj($sigformdict);
# Annotation Widget, contd.
$sigannotdict->{Type} = PDFName("Annot");
$sigannotdict->{Subtype} = PDFName("Widget");
$sigannotdict->{F} = PDFNum(4);
$sigannotdict->{Parent} = $sigformdict;
$sigannotdict->{Rect} = PDF::API2::Basic::PDF::Literal->new("[0 0 0
+ 0]");
$sigannotdict->{P} = $pdf->openpage(1);
$sigannotdict->{H} = PDFName("N");
$sigannotdict = $p->new_obj($sigannotdict);
if ($add_mdp) {
my $permdict = PDF::API2::Basic::PDF::Dict->new();
$permdict->{DocMDP} = $sigdict;
$permdict = $p->new_obj($permdict);
$pdf->{catalog}->{'Perm'} = $permdict;
}
# create AcroForm dictionary
# TODO: if one is present, then only append to it
my @formarray;
push @formarray, $sigformdict;
my $acroformdict = PDF::API2::Basic::PDF::Dict->new();
$acroformdict->{Fields} = PDFArray @formarray;
$acroformdict->{SigFlags} = PDFNum(3);
$acroformdict = $p->new_obj($acroformdict);
$pdf->{catalog}->{'AcroForm'} = $acroformdict;
$pdf->{pdf}->out_obj($pdf->{catalog});
# so now we have the temporary document with zeroes
$pdf->saveas($tempfilename);
say "added AcroForm: $input_filename --> $tempfilename";
sub make_signature {
my $tempfilename = shift;
my $outfilename = shift;
# calc ByteRange
my $data = read_file($tempfilename, { binmode => ':raw' });
my $sig_start = rindex($data, '/Contents <000000000000000000000');
$sig_start += length '/Contents ';
my $sig_end = index($data, '0000>', $sig_start);
$sig_end += length '0000>';
my $sig_trail = length($data) - $sig_end;
my $range = sprintf("/ByteRange [0 %8d %8d %8d]", $sig_start, $sig_e
+nd, $sig_trail);
$data =~ s/\/ByteRange \[0 00000000 00000000 00000000\]/$range/;
say "calc'd $range";
if ($sig_end - $sig_start != 2+2*$sig_length) {
say "Hey, that ByteRange is wrong!";
}
# prepare content for digest
my $plaintext = substr($data, 0, $sig_start) . substr($data, $sig_en
+d, $sig_trail);
my $plaintextfilename = '/tmp/plaintext.pdf';
write_file($plaintextfilename, {binmode => ':raw'}, $plaintext);
say "debug-output, signature input in $plaintextfilename";
if ($sig_algorithm eq 'rsa') {
# calc SHA1
say "SHA-1 is " . sha1_hex($plaintext);
my $digest = sha1($plaintext);
# calc Sig
my $rsa_priv = Crypt::OpenSSL::RSA->new_private_key($priv_key);
$rsa_priv->use_sha1_hash();
$rsa_priv->use_pkcs1_padding();
my $signature = $rsa_priv->sign($digest);
my $rsa_verify = Crypt::OpenSSL::RSA->new_public_key($x509->pubkey
+());
say "calc'd Signature";
if (!$rsa_verify->verify($digest, $signature)) {
say "Hey, that Signature is wrong!";
};
write_file('/tmp/signature.p1', {binmode => ':raw'}, $signature);
say "debug-output, signature in /tmp/signature.p1";
my $sig_enc = PDFStrHex($signature)->as_pdf;
chop $sig_enc; # remove closing '>'
substr($data, $sig_start, length($sig_enc), $sig_enc);
write_file($outfilename, {binmode => ':raw'}, $data);
say "added Signature: $tempfilename --> $outfilename";
} else {
# this is ugly, but there is no Perl interface to openssl's pkcs#7
+ function:
use MIME::Base64;
my $signature = `cat $plaintextfilename | openssl smime -binary -s
+ign -certfile $cacert_filename -signer $x509_filename -inkey $priv_ke
+y_filename | sed -e '1,/^Content-Disposition:/d;/^-----/d;/^\$/d'`;
$signature = decode_base64($signature);
write_file('/tmp/signature.p1', {binmode => ':raw'}, $signature);
say "debug-output, signature in /tmp/signature.p1";
my $sig_enc = PDFStrHex($signature)->as_pdf;
chop $sig_enc; # remove closing '>'
#$sig_enc = substr($sig_enc, 1);
substr($data, $sig_start, length($sig_enc), $sig_enc);
write_file($outfilename, {binmode => ':raw'}, $data);
say "added Signature: $tempfilename --> $outfilename";
}
}
make_signature($tempfilename, $outfilename);
|