Class: HexaPDF::DigitalSignature::Signatures

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/hexapdf/digital_signature/signatures.rb

Overview

This class provides methods for interacting with digital signatures of a PDF file. It is used through HexaPDF::Document#signatures.

Instance Method Summary collapse

Constructor Details

#initialize(document) ⇒ Signatures

Creates a new Signatures object for the given PDF document.



52
53
54
# File 'lib/hexapdf/digital_signature/signatures.rb', line 52

def initialize(document)
  @document = document
end

Instance Method Details

#add(file_or_io, handler, signature: nil, write_options: {}) ⇒ Object

Adds a signature to the document and returns the corresponding signature object.

This method will add a new signature to the document and write the updated document to the given file or IO stream. Afterwards the document can’t be modified anymore and still retain a correct digital signature. To modify the signed document (e.g. for adding another signature) create a new document based on the given file or IO stream instead.

signature

Can either be a signature object (determined via the /Type key), a signature field or nil. Providing a signature object or signature field provides for more control, e.g.:

  • Setting values for optional signature object fields like /Reason and /Location.

  • (In)directly specifying which signature field should be used.

If a signature object is provided and it is not associated with an AcroForm signature field, a new signature field is created and added to the main AcroForm object, creating that if necessary.

If a signature field is provided and it already has a signature object as field value, that signature object is discarded.

If the signature field doesn’t have a widget, a non-visible one is created on the first page.

handler

The signing handler that provides the necessary methods for signing and adjusting the signature and signature field objects to one’s liking, see #signing_handler and Signing::DefaultHandler.

write_options

The key-value pairs of this hash will be passed on to the HexaPDF::Document#write method. Note that incremental will be automatically set to ensure proper behaviour.



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/hexapdf/digital_signature/signatures.rb', line 99

def add(file_or_io, handler, signature: nil, write_options: {})
  if signature && signature.type != :Sig
    signature_field = signature
    signature = signature_field.field_value
  end
  signature ||= @document.add({Type: :Sig})

  # Prepare AcroForm
  form = @document.acro_form(create: true)
  form.signature_flag(:signatures_exist, :append_only)

  # Prepare signature field
  signature_field ||= form.each_field.find {|field| field.field_value == signature } ||
    form.create_signature_field(generate_field_name)
  signature_field.field_value = signature

  if signature_field.each_widget.to_a.empty?
    signature_field.create_widget(@document.pages[0], Rect: [0, 0, 0, 0])
  end

  # Work-around for Adobe Acrobat to recognize images (https://stackoverflow.com/a/73011571/8203541)
  signature_field.each_widget do |widget|
    next unless (resources = widget.appearance&.resources)
    resources[:XObject]&.each do |_name, entry|
      entry[:Resources] ||= {}
    end
  end

  # Prepare signature object
  handler.finalize_objects(signature_field, signature)
  signature[:ByteRange] = [0, 1_000_000_000_000, 1_000_000_000_000, 1_000_000_000_000]
  signature[:Contents] = '00' * handler.signature_size # twice the size due to hex encoding

  io = if file_or_io.kind_of?(String)
         File.open(file_or_io, 'wb+')
       else
         file_or_io
       end

  # Save the current state so that we can determine the correct /ByteRange value and set the
  # values
  start_xref, section = @document.write(io, incremental: true, **write_options)
  signature_offset, signature_length = Signing.locate_signature_dict(section, start_xref,
                                                                     signature.oid)
  io.pos = signature_offset
  signature_data = io.read(signature_length)

  io.seek(0, IO::SEEK_END)
  file_size = io.pos

  # Calculate the offsets for the /ByteRange
  contents_offset = signature_offset + signature_data.index('Contents(') + 8
  offset2 = contents_offset + signature[:Contents].size + 2 # +2 because of the needed < and >
  length2 = file_size - offset2
  signature[:ByteRange] = [0, contents_offset, offset2, length2]

  # Set the correct /ByteRange value
  signature_data.sub!(/ByteRange\[0 1000000000000 1000000000000 1000000000000\]/) do |match|
    length = match.size
    result = "ByteRange[0 #{contents_offset} #{offset2} #{length2}]"
    result.ljust(length)
  end

  # Now everything besides the /Contents value is correct, so we can read the contents for
  # signing
  io.pos = signature_offset
  io.write(signature_data)
  signature[:Contents] = handler.sign(io, signature[:ByteRange].value)

  # And now replace the /Contents value
  Signing.replace_signature_contents(signature_data, signature[:Contents])
  io.pos = signature_offset
  io.write(signature_data)

  signature
ensure
  io.close if io && io != file_or_io
end

#countObject

Returns the number of signatures in the PDF document. May be zero if the document has no signatures.



194
195
196
# File 'lib/hexapdf/digital_signature/signatures.rb', line 194

def count
  each.to_a.size
end

#eachObject

:call-seq:

signatures.each {|signature| block }   -> signatures
signatures.each                        -> Enumerator

Iterates over all signatures in the order they are found in the PDF.



183
184
185
186
187
188
189
190
# File 'lib/hexapdf/digital_signature/signatures.rb', line 183

def each
  return to_enum(__method__) unless block_given?

  return [] unless (form = @document.acro_form)
  form.each_field do |field|
    yield(field.field_value) if field.field_type == :Sig && field.field_value
  end
end

#signing_handler(name: :default, **attributes) ⇒ Object

Creates a signing handler with the given attributes and returns it.

A signing handler name is mapped to a class via the ‘signature.signing_handler’ configuration option. The default signing handler is Signing::DefaultHandler.



60
61
62
63
64
65
# File 'lib/hexapdf/digital_signature/signatures.rb', line 60

def signing_handler(name: :default, **attributes)
  handler = @document.config.constantize('signature.signing_handler', name) do
    raise HexaPDF::Error, "No signing handler named '#{name}' is available"
  end
  handler.new(**attributes)
end