hexapdf 0.28.0 → 0.31.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -10
  3. data/examples/024-digital-signatures.rb +23 -0
  4. data/lib/hexapdf/cli/command.rb +16 -1
  5. data/lib/hexapdf/cli/info.rb +9 -1
  6. data/lib/hexapdf/cli/inspect.rb +2 -2
  7. data/lib/hexapdf/composer.rb +76 -28
  8. data/lib/hexapdf/configuration.rb +29 -16
  9. data/lib/hexapdf/dictionary_fields.rb +13 -4
  10. data/lib/hexapdf/digital_signature/cms_handler.rb +137 -0
  11. data/lib/hexapdf/digital_signature/handler.rb +138 -0
  12. data/lib/hexapdf/digital_signature/pkcs1_handler.rb +96 -0
  13. data/lib/hexapdf/{type → digital_signature}/signature.rb +3 -8
  14. data/lib/hexapdf/digital_signature/signatures.rb +210 -0
  15. data/lib/hexapdf/digital_signature/signing/default_handler.rb +317 -0
  16. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +308 -0
  17. data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +148 -0
  18. data/lib/hexapdf/digital_signature/signing.rb +101 -0
  19. data/lib/hexapdf/{type/signature → digital_signature}/verification_result.rb +37 -41
  20. data/lib/hexapdf/digital_signature.rb +56 -0
  21. data/lib/hexapdf/document/pages.rb +31 -18
  22. data/lib/hexapdf/document.rb +29 -15
  23. data/lib/hexapdf/encryption/standard_security_handler.rb +4 -3
  24. data/lib/hexapdf/filter/flate_decode.rb +20 -8
  25. data/lib/hexapdf/layout/page_style.rb +144 -0
  26. data/lib/hexapdf/layout.rb +1 -0
  27. data/lib/hexapdf/task/optimize.rb +8 -6
  28. data/lib/hexapdf/type/font_simple.rb +14 -2
  29. data/lib/hexapdf/type/object_stream.rb +7 -2
  30. data/lib/hexapdf/type/outline.rb +1 -1
  31. data/lib/hexapdf/type/outline_item.rb +1 -1
  32. data/lib/hexapdf/type/page.rb +29 -8
  33. data/lib/hexapdf/type/xref_stream.rb +11 -4
  34. data/lib/hexapdf/type.rb +0 -1
  35. data/lib/hexapdf/version.rb +1 -1
  36. data/lib/hexapdf/writer.rb +1 -1
  37. data/test/hexapdf/{type/signature → digital_signature}/common.rb +31 -3
  38. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +162 -0
  39. data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +225 -0
  40. data/test/hexapdf/digital_signature/signing/test_timestamp_handler.rb +88 -0
  41. data/test/hexapdf/{type/signature/test_adbe_pkcs7_detached.rb → digital_signature/test_cms_handler.rb} +7 -7
  42. data/test/hexapdf/{type/signature → digital_signature}/test_handler.rb +4 -4
  43. data/test/hexapdf/{type/signature/test_adbe_x509_rsa_sha1.rb → digital_signature/test_pkcs1_handler.rb} +3 -3
  44. data/test/hexapdf/{type → digital_signature}/test_signature.rb +7 -7
  45. data/test/hexapdf/digital_signature/test_signatures.rb +137 -0
  46. data/test/hexapdf/digital_signature/test_signing.rb +53 -0
  47. data/test/hexapdf/{type/signature → digital_signature}/test_verification_result.rb +7 -7
  48. data/test/hexapdf/document/test_pages.rb +25 -0
  49. data/test/hexapdf/encryption/test_standard_security_handler.rb +2 -2
  50. data/test/hexapdf/filter/test_flate_decode.rb +19 -5
  51. data/test/hexapdf/layout/test_page_style.rb +70 -0
  52. data/test/hexapdf/task/test_optimize.rb +11 -9
  53. data/test/hexapdf/test_composer.rb +35 -10
  54. data/test/hexapdf/test_dictionary_fields.rb +9 -3
  55. data/test/hexapdf/test_document.rb +1 -1
  56. data/test/hexapdf/test_writer.rb +8 -8
  57. data/test/hexapdf/type/test_font_simple.rb +18 -6
  58. data/test/hexapdf/type/test_object_stream.rb +16 -7
  59. data/test/hexapdf/type/test_outline.rb +3 -1
  60. data/test/hexapdf/type/test_outline_item.rb +3 -1
  61. data/test/hexapdf/type/test_page.rb +42 -11
  62. data/test/hexapdf/type/test_xref_stream.rb +6 -1
  63. metadata +27 -15
  64. data/lib/hexapdf/document/signatures.rb +0 -546
  65. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +0 -135
  66. data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +0 -95
  67. data/lib/hexapdf/type/signature/handler.rb +0 -140
  68. data/test/hexapdf/document/test_signatures.rb +0 -352
@@ -0,0 +1,144 @@
1
+ # -*- encoding: utf-8; frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # This file is part of HexaPDF.
5
+ #
6
+ # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
+ # Copyright (C) 2014-2023 Thomas Leitner
8
+ #
9
+ # HexaPDF is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU Affero General Public License version 3 as
11
+ # published by the Free Software Foundation with the addition of the
12
+ # following permission added to Section 15 as permitted in Section 7(a):
13
+ # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
14
+ # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
15
+ # INFRINGEMENT OF THIRD PARTY RIGHTS.
16
+ #
17
+ # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
20
+ # License for more details.
21
+ #
22
+ # You should have received a copy of the GNU Affero General Public License
23
+ # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
24
+ #
25
+ # The interactive user interfaces in modified source and object code
26
+ # versions of HexaPDF must display Appropriate Legal Notices, as required
27
+ # under Section 5 of the GNU Affero General Public License version 3.
28
+ #
29
+ # In accordance with Section 7(b) of the GNU Affero General Public
30
+ # License, a covered work must retain the producer line in every PDF that
31
+ # is created or manipulated using HexaPDF.
32
+ #
33
+ # If the GNU Affero General Public License doesn't fit your need,
34
+ # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
+ #++
36
+
37
+ require 'hexapdf/error'
38
+ require 'hexapdf/layout/style'
39
+ require 'hexapdf/layout/frame'
40
+
41
+ module HexaPDF
42
+ module Layout
43
+
44
+ # A PageStyle defines the initial look of a page and the placement of one or more frames.
45
+ class PageStyle
46
+
47
+ # The page size.
48
+ #
49
+ # Can be any valid predefined page size (see HexaPDF::Type::Page::PAPER_SIZE) or an array
50
+ # [llx, lly, urx, ury] specifying a custom page size.
51
+ #
52
+ # Example:
53
+ #
54
+ # style.page_size = :A4
55
+ # style.page_size = [0, 0, 200, 200]
56
+ attr_accessor :page_size
57
+
58
+ # The page orientation, either +:portrait+ or +:landscape+.
59
+ #
60
+ # Only used if #page_size is one of the predefined page sizes and not an array.
61
+ attr_accessor :orientation
62
+
63
+ # A callable object that defines the initial content of a page created with #create_page.
64
+ #
65
+ # The callable object is given a canvas and the page style as arguments. It needs to draw the
66
+ # initial content of the page. Note that the graphics state of the canvas is *not* saved
67
+ # before executing the template code and restored afterwards. If this is needed, the object
68
+ # needs to do it itself.
69
+ #
70
+ # Furthermore it should set the #frame and #next_style attributes appropriately, if not done
71
+ # beforehand. The #create_frame method can be used for easily creating a rectangular frame.
72
+ #
73
+ # Example:
74
+ #
75
+ # page_style.template = lambda do |canvas, style
76
+ # box = canvas.context.box
77
+ # canvas.fill_color("fd0") do
78
+ # canvas.rectangle(0, 0, box.width, box.height).fill
79
+ # end
80
+ # style.frame = style.create_frame(canvas.context, 72)
81
+ # end
82
+ attr_accessor :template
83
+
84
+ # The HexaPDF::Layout::Frame object that defines the area on the page where content should be
85
+ # placed.
86
+ #
87
+ # This can either be set beforehand or during execution of the #template.
88
+ #
89
+ # If no frame has been set, a frame covering the page except for a default margin on all sides
90
+ # is set during #create_page.
91
+ attr_accessor :frame
92
+
93
+ # Defines the name of the page style that should be used for the next page.
94
+ #
95
+ # If this attribute is +nil+ (the default), it means that this style should be used again.
96
+ attr_accessor :next_style
97
+
98
+ # Creates a new page style instance for the given page size and orientation. If a block is
99
+ # given, it is used as template for defining the initial content.
100
+ #
101
+ # Example:
102
+ #
103
+ # PageStyle.new(page_size: :Letter) do |canvas, style|
104
+ # style.frame = style.create_frame(canvas.context, 72)
105
+ # style.next_style = :other
106
+ # canvas.fill_color("fd0") { canvas.circle(100, 100, 50).fill }
107
+ # end
108
+ def initialize(page_size: :A4, orientation: :portrait, &block)
109
+ @page_size = page_size
110
+ @orientation = orientation
111
+ @template = block
112
+ @frame = nil
113
+ @next_style = nil
114
+ end
115
+
116
+ # Creates a new page in the given document with this page style and returns it.
117
+ #
118
+ # If #frame has not been set beforehand or during execution of the #template, a default frame
119
+ # covering the whole page except a margin of 36 is created.
120
+ def create_page(document)
121
+ page = document.pages.create(media_box: page_size, orientation: orientation)
122
+ template&.call(page.canvas, self)
123
+ self.frame ||= create_frame(page, 36)
124
+ page
125
+ end
126
+
127
+ # Creates a frame based on the given page's box and margin.
128
+ #
129
+ # The +margin+ can be any value allowed by HexaPDF::Layout::Style::Quad#set.
130
+ #
131
+ # *Note*: This is a helper method for use inside the #template callable.
132
+ def create_frame(page, margin = 36)
133
+ box = page.box
134
+ margin = Layout::Style::Quad.new(margin)
135
+ Layout::Frame.new(box.left + margin.left,
136
+ box.bottom + margin.bottom,
137
+ box.width - margin.left - margin.right,
138
+ box.height - margin.bottom - margin.top)
139
+ end
140
+
141
+ end
142
+
143
+ end
144
+ end
@@ -56,6 +56,7 @@ module HexaPDF
56
56
  autoload(:ImageBox, 'hexapdf/layout/image_box')
57
57
  autoload(:ColumnBox, 'hexapdf/layout/column_box')
58
58
  autoload(:ListBox, 'hexapdf/layout/list_box')
59
+ autoload(:PageStyle, 'hexapdf/layout/page_style')
59
60
 
60
61
  end
61
62
 
@@ -38,6 +38,8 @@ require 'set'
38
38
  require 'hexapdf/serializer'
39
39
  require 'hexapdf/content/parser'
40
40
  require 'hexapdf/content/operator'
41
+ require 'hexapdf/type/xref_stream'
42
+ require 'hexapdf/type/object_stream'
41
43
 
42
44
  module HexaPDF
43
45
  module Task
@@ -124,7 +126,7 @@ module HexaPDF
124
126
  if object_streams == :generate
125
127
  process_object_streams(doc, :generate, xref_streams)
126
128
  elsif xref_streams == :generate
127
- doc.add({Type: :XRef})
129
+ doc.add({}, type: Type::XRefStream)
128
130
  end
129
131
  end
130
132
 
@@ -150,14 +152,14 @@ module HexaPDF
150
152
  end
151
153
  objects_to_delete.each {|obj| rev.delete(obj) }
152
154
  if xref_streams == :generate && !xref_stream
153
- rev.add(doc.wrap({Type: :XRef}, oid: doc.revisions.next_oid))
155
+ rev.add(doc.wrap({}, type: Type::XRefStream, oid: doc.revisions.next_oid))
154
156
  end
155
157
  end
156
158
  when :generate
157
159
  doc.revisions.each do |rev|
158
160
  xref_stream = false
159
161
  count = 0
160
- objstms = [doc.wrap({Type: :ObjStm})]
162
+ objstms = [doc.wrap({}, type: Type::ObjectStream)]
161
163
  old_objstms = []
162
164
  rev.each do |obj|
163
165
  case obj.type
@@ -173,7 +175,7 @@ module HexaPDF
173
175
  objstms[-1].add_object(obj)
174
176
  count += 1
175
177
  if count == 200
176
- objstms << doc.wrap({Type: :ObjStm})
178
+ objstms << doc.wrap({}, type: Type::ObjectStream)
177
179
  count = 0
178
180
  end
179
181
  end
@@ -182,7 +184,7 @@ module HexaPDF
182
184
  objstm.data.oid = doc.revisions.next_oid
183
185
  rev.add(objstm)
184
186
  end
185
- rev.add(doc.wrap({Type: :XRef}, oid: doc.revisions.next_oid)) unless xref_stream
187
+ rev.add(doc.wrap({}, type: Type::XRefStream, oid: doc.revisions.next_oid)) unless xref_stream
186
188
  end
187
189
  end
188
190
  end
@@ -207,7 +209,7 @@ module HexaPDF
207
209
  xref_stream = true if obj.type == :XRef
208
210
  delete_fields_with_defaults(obj)
209
211
  end
210
- rev.add(doc.wrap({Type: :XRef}, oid: doc.revisions.next_oid)) unless xref_stream
212
+ rev.add(doc.wrap({}, type: Type::XRefStream, oid: doc.revisions.next_oid)) unless xref_stream
211
213
  end
212
214
  end
213
215
  end
@@ -171,9 +171,21 @@ module HexaPDF
171
171
  yield("Required field #{field} is not set", false) if self[field].nil?
172
172
  end
173
173
 
174
+ widths = self[:Widths]
174
175
  if key?(:Widths) && key?(:LastChar) && key?(:FirstChar) &&
175
- self[:Widths].length != (self[:LastChar] - self[:FirstChar] + 1)
176
- yield("Invalid number of entries in field Widths", false)
176
+ widths.length != (self[:LastChar] - self[:FirstChar] + 1)
177
+ yield("Invalid number of entries in field Widths", true)
178
+ difference = self[:LastChar] - self[:FirstChar] + 1 - widths.length
179
+ if difference > 0
180
+ missing_value = if widths.count(widths[0]) == widths.length
181
+ widths[0]
182
+ else
183
+ self[:FontDescriptor]&.[](:MissingWidth) || 0
184
+ end
185
+ difference.times { widths << missing_value }
186
+ else
187
+ widths.slice!(difference, -difference)
188
+ end
177
189
  end
178
190
  end
179
191
 
@@ -101,8 +101,8 @@ module HexaPDF
101
101
  define_type :ObjStm
102
102
 
103
103
  define_field :Type, type: Symbol, required: true, default: type, version: '1.5'
104
- define_field :N, type: Integer # not required, will be auto-filled on #write_objects
105
- define_field :First, type: Integer # not required, will be auto-filled on #write_objects
104
+ define_field :N, type: Integer, required: true
105
+ define_field :First, type: Integer, required: true
106
106
  define_field :Extends, type: Stream
107
107
 
108
108
  # Parses the stream and returns an ObjectStream::Data object that can be used for retrieving
@@ -230,6 +230,11 @@ module HexaPDF
230
230
 
231
231
  # Validates that the generation number of the object stream is zero.
232
232
  def perform_validation
233
+ # Assign dummy values so that the validation for required values works since those values
234
+ # are only set on #write_objects
235
+ self[:N] ||= 0
236
+ self[:First] ||= 0
237
+
233
238
  super
234
239
  yield("Object stream has invalid generation number > 0", false) if gen != 0
235
240
  end
@@ -126,7 +126,7 @@ module HexaPDF
126
126
  if (first && !last) || (!first && last)
127
127
  yield('Outline dictionary is missing an endpoint reference', true)
128
128
  node, dir = first ? [first, :Next] : [last, :Prev]
129
- node = node[dir] while node.key?(dir)
129
+ node = node[dir] while node[dir]
130
130
  self[dir == :Next ? :Last : :First] = node
131
131
  elsif !first && !last && self[:Count] && self[:Count] != 0
132
132
  yield('Outline dictionary key /Count set but no items exist', true)
@@ -397,7 +397,7 @@ module HexaPDF
397
397
  if (first && !last) || (!first && last)
398
398
  yield('Outline item dictionary is missing an endpoint reference', true)
399
399
  node, dir = first ? [first, :Next] : [last, :Prev]
400
- node = node[dir] while node.key?(dir)
400
+ node = node[dir] while node[dir]
401
401
  self[dir == :Next ? :Last : :First] = node
402
402
  elsif !first && !last && self[:Count] && self[:Count] != 0
403
403
  yield('Outline item dictionary key /Count set but no descendants exist', true)
@@ -104,8 +104,16 @@ module HexaPDF
104
104
  Executive: [0, 0, 522, 756].freeze,
105
105
  }.freeze
106
106
 
107
- # Returns the media box for the given paper size. See PAPER_SIZE for the defined paper sizes.
107
+ # Returns the media box for the given paper size or array.
108
+ #
109
+ # If an array is specified, it needs to contain exactly four numbers. The +orientation+
110
+ # argument is not used in this case.
111
+ #
112
+ # See PAPER_SIZE for the defined paper sizes.
108
113
  def self.media_box(paper_size, orientation: :portrait)
114
+ return paper_size if paper_size.kind_of?(Array) && paper_size.size == 4 &&
115
+ paper_size.all?(Numeric)
116
+
109
117
  unless PAPER_SIZE.key?(paper_size)
110
118
  raise HexaPDF::Error, "Invalid paper size specified: #{paper_size}"
111
119
  end
@@ -118,9 +126,6 @@ module HexaPDF
118
126
  # The inheritable fields.
119
127
  INHERITABLE_FIELDS = [:Resources, :MediaBox, :CropBox, :Rotate].freeze
120
128
 
121
- # The required inheritable fields.
122
- REQUIRED_INHERITABLE_FIELDS = [:Resources, :MediaBox].freeze
123
-
124
129
  define_type :Page
125
130
 
126
131
  define_field :Type, type: Symbol, required: true, default: type
@@ -609,10 +614,26 @@ module HexaPDF
609
614
  return unless parent_node
610
615
 
611
616
  super
612
- REQUIRED_INHERITABLE_FIELDS.each do |name|
613
- next if self[name]
614
- yield("Inheritable page field #{name} not set", name == :Resources)
615
- resources.validate(&block) if name == :Ressources
617
+
618
+ unless self[:Resources]
619
+ yield("Required inheritable page field Resources not set", true)
620
+ resources.validate(&block)
621
+ end
622
+
623
+ unless self[:MediaBox]
624
+ yield("Required inheritable page field MediaBox not set", true)
625
+ index = self.index
626
+ box_before = index == 0 ? nil : document.pages[index - 1][:MediaBox]
627
+ box_after = index == document.pages.count - 1 ? nil : document.pages[index + 1]&.[](:MediaBox)
628
+ self[:MediaBox] =
629
+ if box_before && (box_before&.value == box_after&.value || box_after.nil?)
630
+ box_before.dup
631
+ elsif box_after && box_before.nil?
632
+ box_after
633
+ else
634
+ self.class.media_box(document.config['page.default_media_box'],
635
+ orientation: document.config['page.default_media_orientation'])
636
+ end
616
637
  end
617
638
  end
618
639
 
@@ -72,12 +72,10 @@ module HexaPDF
72
72
 
73
73
  define_field :Type, type: Symbol, default: type, required: true, indirect: false,
74
74
  version: '1.5'
75
- # Size is not required because it will be auto-filled before the object is written
76
- define_field :Size, type: Integer, indirect: false
75
+ define_field :Size, type: Integer, indirect: false, required: true
77
76
  define_field :Index, type: PDFArray, indirect: false
78
77
  define_field :Prev, type: Integer, indirect: false
79
- # W is not required because it will be auto-filled on #update_with_xref_section_and_trailer
80
- define_field :W, type: PDFArray, indirect: false
78
+ define_field :W, type: PDFArray, indirect: false, required: true
81
79
 
82
80
  # Returns an XRefSection that represents the content of this cross-reference stream.
83
81
  #
@@ -219,6 +217,15 @@ module HexaPDF
219
217
  [[1, middle, 2], pack_string]
220
218
  end
221
219
 
220
+ def perform_validation #:nodoc
221
+ # Size is not required because it will be auto-filled before the object is written
222
+ # W is not required because it will be auto-filled on #update_with_xref_section_and_trailer
223
+ # Set both here to dummy values to make validation work for the required values
224
+ self[:Size] ||= 1
225
+ self[:W] ||= [1, 1, 1]
226
+ super
227
+ end
228
+
222
229
  end
223
230
 
224
231
  end
data/lib/hexapdf/type.rb CHANGED
@@ -72,7 +72,6 @@ module HexaPDF
72
72
  autoload(:FontType3, 'hexapdf/type/font_type3')
73
73
  autoload(:IconFit, 'hexapdf/type/icon_fit')
74
74
  autoload(:AcroForm, 'hexapdf/type/acro_form')
75
- autoload(:Signature, 'hexapdf/type/signature')
76
75
  autoload(:Outline, 'hexapdf/type/outline')
77
76
  autoload(:OutlineItem, 'hexapdf/type/outline_item')
78
77
  autoload(:PageLabel, 'hexapdf/type/page_label')
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.28.0'
40
+ VERSION = '0.31.0'
41
41
 
42
42
  end
@@ -207,7 +207,7 @@ module HexaPDF
207
207
  end
208
208
 
209
209
  if (!object_streams.empty? || @use_xref_streams) && xref_stream.nil?
210
- xref_stream = @document.wrap({Type: :XRef}, oid: @document.revisions.next_oid)
210
+ xref_stream = @document.wrap({}, type: Type::XRefStream, oid: @document.revisions.next_oid)
211
211
  rev.add(xref_stream)
212
212
  end
213
213
 
@@ -6,7 +6,7 @@ module HexaPDF
6
6
  class Certificates
7
7
 
8
8
  def ca_key
9
- @ca_key ||= OpenSSL::PKey::RSA.new(512)
9
+ @ca_key ||= OpenSSL::PKey::RSA.new(2048)
10
10
  end
11
11
 
12
12
  def ca_certificate
@@ -36,13 +36,17 @@ module HexaPDF
36
36
  end
37
37
 
38
38
  def signer_key
39
- @signer_key ||= OpenSSL::PKey::RSA.new(512)
39
+ @signer_key ||= OpenSSL::PKey::RSA.new(2048)
40
+ end
41
+
42
+ def dsa_signer_key
43
+ @dsa_signer_key ||= OpenSSL::PKey::DSA.new(2048)
40
44
  end
41
45
 
42
46
  def signer_certificate
43
47
  @signer_certificate ||=
44
48
  begin
45
- name = OpenSSL::X509::Name.parse('/CN=signer/DC=gettalong')
49
+ name = OpenSSL::X509::Name.parse('/CN=RSA signer/DC=gettalong')
46
50
 
47
51
  signer_cert = OpenSSL::X509::Certificate.new
48
52
  signer_cert.serial = 2
@@ -65,6 +69,30 @@ module HexaPDF
65
69
  end
66
70
  end
67
71
 
72
+ def dsa_signer_certificate
73
+ @dsa_signer_certificate ||=
74
+ begin
75
+ signer_cert = OpenSSL::X509::Certificate.new
76
+ signer_cert.serial = 3
77
+ signer_cert.version = 2
78
+ signer_cert.not_before = Time.now - 86400
79
+ signer_cert.not_after = Time.now + 86400
80
+ signer_cert.public_key = dsa_signer_key.public_key
81
+ signer_cert.subject = OpenSSL::X509::Name.parse('/CN=DSA signer/DC=gettalong')
82
+ signer_cert.issuer = ca_certificate.subject
83
+
84
+ extension_factory = OpenSSL::X509::ExtensionFactory.new
85
+ extension_factory.subject_certificate = signer_cert
86
+ extension_factory.issuer_certificate = ca_certificate
87
+ signer_cert.add_extension(extension_factory.create_extension('subjectKeyIdentifier', 'hash'))
88
+ signer_cert.add_extension(extension_factory.create_extension('basicConstraints', 'CA:FALSE'))
89
+ signer_cert.add_extension(extension_factory.create_extension('keyUsage', 'digitalSignature'))
90
+ signer_cert.sign(ca_key, OpenSSL::Digest.new('SHA1'))
91
+
92
+ signer_cert
93
+ end
94
+ end
95
+
68
96
  def timestamp_certificate
69
97
  @timestamp_certificate ||=
70
98
  begin
@@ -0,0 +1,162 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+ require_relative '../common'
6
+
7
+ describe HexaPDF::DigitalSignature::Signing::DefaultHandler do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @handler = HexaPDF::DigitalSignature::Signing::DefaultHandler.new(
11
+ certificate: CERTIFICATES.signer_certificate,
12
+ key: CERTIFICATES.signer_key,
13
+ certificate_chain: [CERTIFICATES.ca_certificate]
14
+ )
15
+ end
16
+
17
+ it "defaults to standard CMS signatures" do
18
+ assert_equal(:cms, @handler.signature_type)
19
+ end
20
+
21
+ it "returns the size of serialized signature" do
22
+ assert(@handler.signature_size > 1000)
23
+ @handler.signature_size = 100
24
+ assert_equal(100, @handler.signature_size)
25
+ end
26
+
27
+ it "allows setting the DocMDP permissions" do
28
+ assert_nil(@handler.doc_mdp_permissions)
29
+
30
+ @handler.doc_mdp_permissions = :no_changes
31
+ assert_equal(1, @handler.doc_mdp_permissions)
32
+ @handler.doc_mdp_permissions = 1
33
+ assert_equal(1, @handler.doc_mdp_permissions)
34
+
35
+ @handler.doc_mdp_permissions = :form_filling
36
+ assert_equal(2, @handler.doc_mdp_permissions)
37
+ @handler.doc_mdp_permissions = 2
38
+ assert_equal(2, @handler.doc_mdp_permissions)
39
+
40
+ @handler.doc_mdp_permissions = :form_filling_and_annotations
41
+ assert_equal(3, @handler.doc_mdp_permissions)
42
+ @handler.doc_mdp_permissions = 3
43
+ assert_equal(3, @handler.doc_mdp_permissions)
44
+
45
+ @handler.doc_mdp_permissions = nil
46
+ assert_nil(@handler.doc_mdp_permissions)
47
+
48
+ assert_raises(ArgumentError) { @handler.doc_mdp_permissions = :other }
49
+ end
50
+
51
+ describe "sign" do
52
+ it "can sign the data using the provided certificate and key" do
53
+ data = StringIO.new("data")
54
+ signed_data = @handler.sign(data, [0, data.string.size, 0, 0])
55
+
56
+ pkcs7 = OpenSSL::PKCS7.new(signed_data)
57
+ assert(pkcs7.detached?)
58
+ assert_equal([CERTIFICATES.signer_certificate, CERTIFICATES.ca_certificate],
59
+ pkcs7.certificates)
60
+ store = OpenSSL::X509::Store.new
61
+ store.add_cert(CERTIFICATES.ca_certificate)
62
+ assert(pkcs7.verify([], store, data.string, OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY))
63
+ end
64
+
65
+ it "can change the used digest algorithm" do
66
+ @handler.digest_algorithm = 'sha384'
67
+ asn1 = OpenSSL::ASN1.decode(@handler.sign(StringIO.new('data'), [0, 4, 0, 0]))
68
+ assert_equal('SHA384', asn1.value[1].value[0].value[1].value[0].value[0].value)
69
+ end
70
+
71
+ it "can embed a timestamp token" do
72
+ @handler.timestamp_handler = tsh = Object.new
73
+ tsh.define_singleton_method(:sign) {|_, _| OpenSSL::ASN1::OctetString.new("signed-tsh") }
74
+ signed = @handler.sign(StringIO.new('data'), [0, 4, 0, 0])
75
+ asn1 = OpenSSL::ASN1.decode(signed)
76
+ assert_equal('signed-tsh', asn1.value[1].value[0].value[4].value[0].
77
+ value[6].value[0].value[1].value[0].value)
78
+ end
79
+
80
+ it "creates PAdES compatible signatures" do
81
+ @handler.signature_type = :pades
82
+ signed = @handler.sign(StringIO.new('data'), [0, 4, 0, 0])
83
+ asn1 = OpenSSL::ASN1.decode(signed)
84
+ # check by absence of signing-time signed attribute
85
+ refute(asn1.value[1].value[0].value[4].value[0].value[3].value.
86
+ find {|obj| obj.value[0].value == 'signingTime' })
87
+ end
88
+
89
+ it "can use external signing without certificate set" do
90
+ @handler.certificate = nil
91
+ @handler.external_signing = proc { "hallo" }
92
+ assert_equal("hallo", @handler.sign(StringIO.new, [0, 0, 0, 0]))
93
+ end
94
+
95
+ it "can use external signing with certificate set but not the key" do
96
+ @handler.key = nil
97
+ @handler.external_signing = proc do |algorithm, _hash|
98
+ assert_equal('sha256', algorithm)
99
+ "hallo"
100
+ end
101
+ result = @handler.sign(StringIO.new, [0, 0, 0, 0])
102
+ asn1 = OpenSSL::ASN1.decode(result)
103
+ assert_equal("hallo", asn1.value[1].value[0].value[4].value[0].value[5].value)
104
+ end
105
+ end
106
+
107
+ describe "finalize_objects" do
108
+ before do
109
+ @field = @doc.wrap({})
110
+ @obj = @doc.wrap({})
111
+ end
112
+
113
+ it "only sets the mandatory values if no concrete finalization tasks need to be done" do
114
+ @handler.finalize_objects(@field, @obj)
115
+ assert(@field.empty?)
116
+ assert_equal(:'Adobe.PPKLite', @obj[:Filter])
117
+ assert_equal(:'adbe.pkcs7.detached', @obj[:SubFilter])
118
+ assert_kind_of(Time, @obj[:M])
119
+ end
120
+
121
+ it "adjust the /SubFilter if signature type is pades" do
122
+ @handler.signature_type = :pades
123
+ @handler.finalize_objects(@field, @obj)
124
+ assert_equal(:'ETSI.CAdES.detached', @obj[:SubFilter])
125
+ end
126
+
127
+ it "sets the reason, location and contact info fields" do
128
+ @handler.reason = 'Reason'
129
+ @handler.location = 'Location'
130
+ @handler.contact_info = 'Contact'
131
+ @handler.finalize_objects(@field, @obj)
132
+ assert(@field.empty?)
133
+ assert_equal(['Reason', 'Location', 'Contact'], @obj.value.values_at(:Reason, :Location, :ContactInfo))
134
+ end
135
+
136
+ it "fills the build properties dictionary with appropriate application information" do
137
+ @handler.finalize_objects(@field, @obj)
138
+ assert_equal(:HexaPDF, @obj[:Prop_Build][:App][:Name])
139
+ assert_equal(HexaPDF::VERSION, @obj[:Prop_Build][:App][:REx])
140
+ end
141
+
142
+ it "applies the specified DocMDP permissions" do
143
+ @handler.doc_mdp_permissions = :no_changes
144
+ @handler.finalize_objects(@field, @obj)
145
+ ref = @obj[:Reference][0]
146
+ assert_equal(:DocMDP, ref[:TransformMethod])
147
+ assert_equal(:SHA256, ref[:DigestMethod])
148
+ assert_equal(1, ref[:TransformParams][:P])
149
+ assert_equal(:'1.2', ref[:TransformParams][:V])
150
+ assert_same(@obj, @doc.catalog[:Perms][:DocMDP])
151
+ end
152
+
153
+ it "fails if DocMDP should be set but there is already a signature" do
154
+ @handler.doc_mdp_permissions = :no_changes
155
+ 2.times do
156
+ field = @doc.acro_form(create: true).create_signature_field('test')
157
+ field.field_value = :something
158
+ end
159
+ assert_raises(HexaPDF::Error) { @handler.finalize_objects(@field, @obj) }
160
+ end
161
+ end
162
+ end