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, TimestampHandler

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(document) ⇒ Signatures

Creates a new Signatures object for the given PDF document.



385
386
387
# File 'lib/hexapdf/document/signatures.rb', line 385

def initialize(document)
  @document = document
end

Class Method Details

.embed_signature(io, signature) ⇒ Object

Embeds the given signature into the /Contents value of the newest signature dictionary of the PDF document given by the io argument.

This functionality can be used together with the support for external signing (see DefaultHandler and DefaultHandler#external_signing) to implement asynchronous signing.

Note: This will, most probably, only work on documents prepared for external signing by HexaPDF and not by other libraries.



342
343
344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/hexapdf/document/signatures.rb', line 342

def self.embed_signature(io, signature)
  doc = HexaPDF::Document.new(io: io)
  signature_dict = doc.signatures.find {|sig| doc.revisions.current.object(sig) == sig }
  signature_dict_offset, signature_dict_length = locate_signature_dict(
    doc.revisions.current.xref_section,
    doc.revisions.parser.startxref_offset,
    signature_dict.oid
  )
  io.pos = signature_dict_offset
  signature_data = io.read(signature_dict_length)
  replace_signature_contents(signature_data, signature)
  io.pos = signature_dict_offset
  io.write(signature_data)
end

.locate_signature_dict(xref_section, start_xref_position, signature_oid) ⇒ Object

Uses the information in the given cross-reference section as well as the byte offset of the cross-reference section to calculate the offset and length of the signature dictionary with the given object id.



360
361
362
363
364
365
# File 'lib/hexapdf/document/signatures.rb', line 360

def self.locate_signature_dict(xref_section, start_xref_position, signature_oid)
  data = xref_section.map {|oid, _gen, entry| [entry.pos, oid] if entry.in_use? }.compact.sort <<
    [start_xref_position, nil]
  index = data.index {|_pos, oid| oid == signature_oid }
  [data[index][0], data[index + 1][0] - data[index][0]]
end

.replace_signature_contents(signature_data, contents) ⇒ Object

Replaces the value of the /Contents key in the serialized signature_data with the value of contents.



369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/hexapdf/document/signatures.rb', line 369

def self.replace_signature_contents(signature_data, contents)
  signature_data.sub!(/Contents(?:\(.*?\)|<.*?>)/) do |match|
    length = match.size
    result = "Contents<#{contents.unpack1('H*')}"
    if length < result.size
      raise HexaPDF::Error, "The reserved space for the signature was too small " \
        "(#{(length - 10) / 2} vs #{(result.size - 10) / 2}) - use the handlers " \
        "#signature_size method to increase the reserved space"
    end
    "#{result.ljust(length - 1, '0')}>"
  end
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 #handler and 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.

The used signature object will have the following default values set:

/Filter

/Adobe.PPKLite

/SubFilter

/adbe.pkcs7.detached

/M

The current time.

These values can be overridden in the #finalize_objects method of the signature handler.



439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
# File 'lib/hexapdf/document/signatures.rb', line 439

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] = :'Adobe.PPKLite'
  signature[:SubFilter] = :'adbe.pkcs7.detached'
  signature[:M] = Time.now
  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 = self.class.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
  self.class.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.



529
530
531
# File 'lib/hexapdf/document/signatures.rb', line 529

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.



518
519
520
521
522
523
524
525
# File 'lib/hexapdf/document/signatures.rb', line 518

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, **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 DefaultHandler.



393
394
395
396
397
398
# File 'lib/hexapdf/document/signatures.rb', line 393

def 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