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:

  1. No changes.
  2. Filling in forms and signing.
  3. 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:

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')