Class: HexaPDF::Document::Signatures

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

Overview

This class provides methods for interacting with digital signatures of a PDF file.

Defined Under Namespace

Classes: DefaultHandler

Instance Method Summary collapse

Constructor Details

#initialize(document) ⇒ Signatures

Creates a new Signatures object for the given PDF document.



167
168
169
# File 'lib/hexapdf/document/signatures.rb', line 167

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; create a new document based on the 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 #handler and DefaultHandler.

write_options

The key-value pairs of this hash will be passed on to the HexaPDF::Document#write command. Note that incremental will be automatically set if signing an already existing file.



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/hexapdf/document/signatures.rb', line 213

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

  # Prepare signature object
  signature[:Filter] = handler.filter_name
  signature[:SubFilter] = handler.sub_filter_name
  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
  signature[:M] = Time.now

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

  # Save the current state so that we can determine the correct /ByteRange value and set the
  # values
  handler.finalize_objects(signature_field, signature)
  section = @document.write(io, incremental: true, **write_options)
  data = section.map {|oid, _gen, entry| [entry.pos, oid] if entry.in_use? }.compact.sort
  index = data.index {|_pos, oid| oid == signature.oid }
  signature_offset = data[index][0]
  signature_length = data[index + 1][0] - data[index][0]
  io.pos = signature_offset
  signature_data = io.read(signature_length)

  io.rewind
  file_data = io.read

  # 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_data.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
  file_data[signature_offset, signature_length] = signature_data
  signed_contents = file_data[0, contents_offset] << file_data[offset2, length2]
  signature[:Contents] = handler.sign(signed_contents)

  # Set the correct /Contents value as hexstring
  signature_data.sub!(/Contents\(0+\)/) do |match|
    length = match.size
    result = "Contents<#{signature[:Contents].unpack1('H*')}"
    "#{result.ljust(length - 1, '0')}>"
  end

  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.



310
311
312
# File 'lib/hexapdf/document/signatures.rb', line 310

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.



299
300
301
302
303
304
305
306
# File 'lib/hexapdf/document/signatures.rb', line 299

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

#handler(name: :default, **options) ⇒ Object

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

A signing handler name is mapped to a class via the 'signature.signing_handler' configuration option.



175
176
177
178
179
180
# File 'lib/hexapdf/document/signatures.rb', line 175

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