Signing PDFs
This how-to guide provides information and code samples for applying digital signatures to PDF files. It is assumed that you are familiar with digital signatures, if not, have a look at the Digital Signatures key topic.
All the examples can be run with or without a PDF file supplied as first argument, e.g. copying the
code and pasting it to a running ruby
process just works. The examples use the demo certificates
that are included with the HexaPDF distribution. For real world usage you have to provide your own
signing certificate!
Applying a Single Signature
A digital signature can be applied to an existing document or to a new one. The steps for applying the digital signature are the same and must be done after everything else.
When applying a signature to an existing document, the document itself won’t be changed but a new
revision with the signature will be added by default. However, if needed it is also possible to
include the digital signature in the original file by setting the incremental: false
write option.
In this sample code we are using a local signing certificate and the default signing handler to add a non-visual signature. Everything needed for a correct signature (AcroForm signature field and signature object) is automatically created with default values.
require 'hexapdf'
require HexaPDF.data_dir + '/cert/demo_cert.rb'
doc = if ARGV[0]
HexaPDF::Document.open(ARGV[0])
else
HexaPDF::Document.new.tap do |hpdoc|
hpdoc.pages.add(:A6, orientation: :landscape).canvas.
font('Helvetica', size: 18).
text('Digital Signature Test - Single Signature', at: [10, 150])
end
end
doc.sign("signed.pdf", reason: 'Some reason',
certificate: HexaPDF.demo_cert.cert,
key: HexaPDF.demo_cert.key,
certificate_chain: [HexaPDF.demo_cert.sub_ca,
HexaPDF.demo_cert.root_ca])
Adding a Visual Appearance
If a visual signature is needed, we need to create it before signing the document. It is generally possible to construct the document in a way to modify the visual appearance after signing. However, this is a bit more involved and the signature might not show correctly in all viewers.
The visual appearance needs to be set on the signature field associated with the signature. This means we need to create the signature field beforehand and create its appearance stream; and then pass that field to the signing method.
The following code is nearly the same as the one for applying a single signature, we just add the necessary appearance creation.
require 'hexapdf'
require HexaPDF.data_dir + '/cert/demo_cert.rb'
doc = if ARGV[0]
HexaPDF::Document.open(ARGV[0])
else
HexaPDF::Document.new.tap do |hpdoc|
hpdoc.pages.add(:A6, orientation: :landscape).canvas.
font('Helvetica', size: 18).
text('Digital Signature Test - Visual Appearance', at: [10, 150])
end
end
sig_field = doc.acro_form(create: true).create_signature_field('signature')
widget = sig_field.create_widget(doc.pages[0], Rect: [20, 20, 120, 120])
widget.create_appearance.canvas.
stroke_color("red").rectangle(1, 1, 99, 99).stroke.
font("Helvetica", size: 10).
text("Certified by signer", at: [10, 10])
doc.sign("signed.pdf", signature: sig_field,
reason: 'Some reason',
certificate: HexaPDF.demo_cert.cert,
key: HexaPDF.demo_cert.key,
certificate_chain: [HexaPDF.demo_cert.sub_ca,
HexaPDF.demo_cert.root_ca])
Applying Multiple Signatures
Applying multiple signatures to a PDF requires performing the same signing steps just multiple times. And the PDF’s permissions set with the first signature must allow such changes so that all prior signatures stay valid.
Since the document must be written each time a signature is applied, it has to be reloaded for each additional signature.
The following code signs the input document three times with the same signature. One might also use existing signature fields for the signatures if necessary.
require 'hexapdf'
require 'stringio'
require HexaPDF.data_dir + '/cert/demo_cert.rb'
in_io = StringIO.new
if ARGV[0]
in_io.string = File.binread(ARGV[0])
else
HexaPDF::Document.new.tap do |hpdoc|
hpdoc.pages.add(:A6, orientation: :landscape).canvas.
font('Helvetica', size: 18).
text('Digital Signature Test - Multiple Signatures', at: [10, 150])
end.write(in_io)
end
3.times do |i|
doc = HexaPDF::Document.new(io: in_io)
out_io = StringIO.new(''.b)
doc.sign(out_io, reason: "Some reason #{i}",
certificate: HexaPDF.demo_cert.cert,
key: HexaPDF.demo_cert.key,
certificate_chain: [HexaPDF.demo_cert.sub_ca,
HexaPDF.demo_cert.root_ca])
in_io = out_io
end
File.binwrite('signed.pdf', in_io.string)
Applying a PAdES Signature
HexaPDF creates the older CMS signatures by default but also supports the newer PAdES signatures. To
create a PAdES compatible signature, the DefaultHandler#signature_type attribute has to be set to
:pades
.
If no timestamp handler is set via DefaultHandler#timestamp_handler, the created signature is a PAdES-BES level B-B signature. Otherwise it is a level B-T signature.
The following example creates a level B-T signature:
require 'hexapdf'
require HexaPDF.data_dir + '/cert/demo_cert.rb'
doc = if ARGV[0]
HexaPDF::Document.open(ARGV[0])
else
HexaPDF::Document.new.tap do |hpdoc|
hpdoc.pages.add(:A6, orientation: :landscape).canvas.
font('Helvetica', size: 18).
text('Digital Signature Test - PAdES Signature', at: [10, 150])
end
end
ts_handler = doc.signatures.signing_handler(name: :timestamp,
tsa_url: 'http://freetsa.org/tsr')
doc.sign("signed.pdf", reason: 'Some reason',
signature_type: :pades,
timestamp_handler: ts_handler,
certificate: HexaPDF.demo_cert.cert,
key: HexaPDF.demo_cert.key,
certificate_chain: [HexaPDF.demo_cert.sub_ca,
HexaPDF.demo_cert.root_ca])
Finalizing a PDF vs Allowing Some Modifications
When applying the first signature it is possible to finalize the PDF so that no or only some modifications are allowed. This is called creating a certification signature.
It is possible to set the following permissions:
- No changes.
- Filling in forms and signing.
- Filling in forms, signing and annotation creation, deletion and modification.
The following code creates a signed document that allows no modifications:
require 'hexapdf'
require HexaPDF.data_dir + '/cert/demo_cert.rb'
doc = if ARGV[0]
HexaPDF::Document.open(ARGV[0])
else
HexaPDF::Document.new.tap do |hpdoc|
hpdoc.pages.add(:A6, orientation: :landscape).canvas.
font('Helvetica', size: 18).
text('Digital Signature Test - Certification Signature', at: [10, 150])
end
end
doc.sign("signed.pdf", doc_mdp_permissions: :no_changes,
certificate: HexaPDF.demo_cert.cert,
key: HexaPDF.demo_cert.key,
certificate_chain: [HexaPDF.demo_cert.sub_ca,
HexaPDF.demo_cert.root_ca])
Using an External Mechanism for Signing
Sometimes the built-in mechanism for signing cannot be used because the private key is not readily available. Such situations often arise when a hardware security module (HSM) is used for signing since it doesn’t allow extracting the private key.
In such cases signing itself has to be done by the application using HexaPDF. And since HexaPDF doesn’t know anything about the signature, one also has to tell HexaPDF the signature size so that enough space is reserved.
The following code uses the demo certificate together with the external signing mechanism for
signing the data but let’s HexaPDF handle the CMS signature creation (note the assignment of the
certificate
):
require 'hexapdf'
require HexaPDF.data_dir + '/cert/demo_cert.rb'
doc = if ARGV[0]
HexaPDF::Document.open(ARGV[0])
else
HexaPDF::Document.new.tap do |hpdoc|
hpdoc.pages.add(:A6, orientation: :landscape).canvas.
font('Helvetica', size: 18).
text('Digital Signature Test - External Signing', at: [10, 150])
end
end
signing_mechanism = lambda do |digest_method, data|
HexaPDF.demo_cert.key.sign_raw(digest_method, data)
end
doc.sign("signed.pdf", external_signing: signing_mechanism,
certificate: HexaPDF.demo_cert.cert,
certificate_chain: [HexaPDF.demo_cert.sub_ca,
HexaPDF.demo_cert.root_ca])
If HexaPDF is not able to create the CMS signature, e.g. because it doesn’t support the
certificate’s key algorithm, it is also possible to delegate the CMS signature creation completely
to the external signing mechanism (note the absence of the certificate
key in
HexaPDF::Document#sign):
require 'hexapdf'
require HexaPDF.data_dir + '/cert/demo_cert.rb'
doc = if ARGV[0]
HexaPDF::Document.open(ARGV[0])
else
HexaPDF::Document.new.tap do |hpdoc|
hpdoc.pages.add(:A6, orientation: :landscape).canvas.
font('Helvetica', size: 18).
text('Digital Signature Test - External Signing', at: [10, 150])
end
end
signing_mechanism = lambda do |io, byte_range|
# Read the bytes to be signed
io.pos = byte_range[0]
data = io.read(byte_range[1])
io.pos = byte_range[2]
data << io.read(byte_range[3])
# Create the DER encoded CMS signed-data structure
OpenSSL::PKCS7.sign(HexaPDF.demo_cert.cert, HexaPDF.demo_cert.key,
data, [HexaPDF.demo_cert.sub_ca, HexaPDF.demo_cert.root_ca],
OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY).to_der
end
doc.sign("signed.pdf", signature_size: 10_000, external_signing: signing_mechanism)
Asynchronous External Signing
There are some situations where between the creation of the prepared to-be-signed document and the creation of the digital signature itself lies some time. This might happen in a web application where users can sign PDF documents using a client-local mechanism, for example, by using some citizen ID signature system.
In such cases a variant of the external signing mechanism can be used:
-
Pretend to sign the document using the external signing mechanism but really just return an empty string. The written document now contains everything but the actual digital signature.
-
Create the digital signature based on the information (IO object and byte range array) provided to the external signing mechanism.
-
Embed the digital signature into the prepared document, getting the final document.
The following code does exactly this:
require 'hexapdf'
require HexaPDF.data_dir + '/cert/demo_cert.rb'
doc = if ARGV[0]
HexaPDF::Document.open(ARGV[0])
else
HexaPDF::Document.new.tap do |hpdoc|
hpdoc.pages.add(:A6, orientation: :landscape).canvas.
font('Helvetica', size: 18).
text('Digital Signature Test - Async External Signing', at: [10, 150])
end
end
# Prepare the document for embedding of the digital signature
data = nil # Used for storing the to-be-signed data
signing_mechanism = lambda do |io, byte_range|
# Store the to-be-signed data in the local variable data
io.pos = byte_range[0]
data = io.read(byte_range[1])
io.pos = byte_range[2]
data << io.read(byte_range[3])
""
end
doc.sign("signed.pdf", signature_size: 10_000, external_signing: signing_mechanism)
# Create the signature
signature = OpenSSL::PKCS7.sign(HexaPDF.demo_cert.cert, HexaPDF.demo_cert.key, data,
[HexaPDF.demo_cert.sub_ca, HexaPDF.demo_cert.root_ca],
OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY).to_der
# Embed the signature
HexaPDF::DigitalSignature::Signing.embed_signature(File.open('signed.pdf', 'rb+'), signature)
Applying a Timestamp Signature
Timestamp signatures are similar to standard signatures in that they use the same signing infrastructure. The differences lie in some signature dictionary field values and the structure of the digital signature itself. Therefore it is only necessary to use a special signing handler for creating timestamp signatures.
It is mandatory to provide at least the URL of the timestamp authority server (TSA), everything else is optional and uses default values. Note that the server will be contacted twice if you don’t specify the signature size!
The following code use the timestamp service from https://freeTSA.org to add a timestamp to a PDF document:
require 'hexapdf'
doc = if ARGV[0]
HexaPDF::Document.open(ARGV[0])
else
HexaPDF::Document.new.tap do |hpdoc|
hpdoc.pages.add(:A6, orientation: :landscape).canvas.
font('Helvetica', size: 18).
text('Digital Signature Test - Timestamp Signature', at: [10, 150])
end
end
doc.sign("signed.pdf", handler: :timestamp, reason: 'Some reason',
signature_size: 20_000,
tsa_url: 'https://freetsa.org/tsr')