hexapdf 0.26.2 → 0.27.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -0
  3. data/README.md +1 -1
  4. data/examples/013-text_layouter_shapes.rb +8 -8
  5. data/examples/016-frame_automatic_box_placement.rb +3 -3
  6. data/examples/017-frame_text_flow.rb +3 -3
  7. data/examples/020-column_box.rb +3 -3
  8. data/lib/hexapdf/cli/split.rb +7 -7
  9. data/lib/hexapdf/cli/watermark.rb +2 -2
  10. data/lib/hexapdf/configuration.rb +2 -0
  11. data/lib/hexapdf/dictionary.rb +3 -12
  12. data/lib/hexapdf/document/destinations.rb +42 -5
  13. data/lib/hexapdf/document/signatures.rb +265 -48
  14. data/lib/hexapdf/importer.rb +3 -0
  15. data/lib/hexapdf/parser.rb +1 -0
  16. data/lib/hexapdf/revisions.rb +3 -1
  17. data/lib/hexapdf/tokenizer.rb +2 -2
  18. data/lib/hexapdf/type/acro_form/form.rb +28 -1
  19. data/lib/hexapdf/type/catalog.rb +1 -1
  20. data/lib/hexapdf/type/outline.rb +18 -0
  21. data/lib/hexapdf/type/outline_item.rb +72 -14
  22. data/lib/hexapdf/type/page.rb +56 -35
  23. data/lib/hexapdf/type/resources.rb +13 -17
  24. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +16 -2
  25. data/lib/hexapdf/type/signature.rb +10 -0
  26. data/lib/hexapdf/version.rb +1 -1
  27. data/lib/hexapdf/writer.rb +3 -0
  28. data/test/hexapdf/document/test_destinations.rb +41 -0
  29. data/test/hexapdf/document/test_signatures.rb +139 -19
  30. data/test/hexapdf/test_importer.rb +14 -0
  31. data/test/hexapdf/test_parser.rb +2 -2
  32. data/test/hexapdf/test_revisions.rb +20 -12
  33. data/test/hexapdf/test_tokenizer.rb +11 -1
  34. data/test/hexapdf/test_writer.rb +11 -3
  35. data/test/hexapdf/type/acro_form/test_form.rb +47 -0
  36. data/test/hexapdf/type/signature/common.rb +52 -0
  37. data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +21 -0
  38. data/test/hexapdf/type/test_catalog.rb +5 -2
  39. data/test/hexapdf/type/test_outline.rb +1 -1
  40. data/test/hexapdf/type/test_outline_item.rb +62 -1
  41. data/test/hexapdf/type/test_page.rb +41 -20
  42. data/test/hexapdf/type/test_resources.rb +0 -5
  43. data/test/hexapdf/type/test_signature.rb +8 -0
  44. data/test/test_helper.rb +1 -1
  45. metadata +17 -3
@@ -126,6 +126,11 @@ module HexaPDF
126
126
  lister: "flags", getter: "flagged?", setter: "flag", unsetter: "unflag",
127
127
  value_getter: "self[:F]", value_setter: "self[:F]")
128
128
 
129
+ # Returns +true+ since outline items must always be indirect objects.
130
+ def must_be_indirect?
131
+ true
132
+ end
133
+
129
134
  # :call-seq:
130
135
  # item.title -> title
131
136
  # item.title(value) -> title
@@ -198,9 +203,46 @@ module HexaPDF
198
203
  end
199
204
  end
200
205
 
206
+ # Returns the outline level this item is one.
207
+ #
208
+ # The level of the items in the main outline dictionary, the root level, is 1.
209
+ #
210
+ # Here is an illustrated example of items contained in a document outline with their
211
+ # associated level:
212
+ #
213
+ # Outline dictionary 0
214
+ # Outline item 1 1
215
+ # |- Sub item 1 2
216
+ # |- Sub item 2 2
217
+ # |- Sub sub item 1 3
218
+ # |- Sub item 3 2
219
+ # Outline item 2 1
220
+ def level
221
+ count = 0
222
+ temp = self
223
+ count += 1 while (temp = temp[:Parent])
224
+ count
225
+ end
226
+
227
+ # Returns the destination page if there is any.
228
+ #
229
+ # * If a destination is set, the associated page is returned.
230
+ # * If an action is set and it is a GoTo action, the associated page is returned.
231
+ # * Otherwise +nil+ is returned.
232
+ def destination_page
233
+ dest = self[:Dest]
234
+ dest = action[:D] if !dest && (action = self[:A]) && action[:S] == :GoTo
235
+ document.destinations.resolve(dest)&.page
236
+ end
237
+
201
238
  # Adds, as child to this item, a new outline item with the given title that performs the
202
239
  # provided action on clicking. Returns the newly added item.
203
240
  #
241
+ # Alternatively, it is possible to provide an already initialized outline item instead of the
242
+ # title. If so, the only other argument that is used is +position+. Existing fields /Prev,
243
+ # /Next, /First, /Last, /Parent and /Count are deleted from the given item and set
244
+ # appropriately.
245
+ #
204
246
  # If neither :destination nor :action is specified, the outline item has no associated action.
205
247
  # This is only meaningful if the new item will have children as it then acts just as a
206
248
  # container.
@@ -251,16 +293,30 @@ module HexaPDF
251
293
  # end
252
294
  def add_item(title, destination: nil, action: nil, position: :last, open: true,
253
295
  text_color: nil, flags: nil) # :yield: item
254
- item = document.add({Parent: self}, type: :XXOutlineItem)
255
- item.title(title)
256
- if action
257
- item.action(action)
296
+ if title.kind_of?(HexaPDF::Object) && title.type == :XXOutlineItem
297
+ item = title
298
+ item.delete(:Prev)
299
+ item.delete(:Next)
300
+ item.delete(:First)
301
+ item.delete(:Last)
302
+ if item[:Count] && item[:Count] >= 0
303
+ item[:Count] = 0
304
+ else
305
+ item.delete(:Count)
306
+ end
307
+ item[:Parent] = self
258
308
  else
259
- item.destination(destination)
309
+ item = document.add({Parent: self}, type: :XXOutlineItem)
310
+ item.title(title)
311
+ if action
312
+ item.action(action)
313
+ else
314
+ item.destination(destination)
315
+ end
316
+ item.text_color(text_color) if text_color
317
+ item.flag(*flags) if flags
318
+ item[:Count] = 0 if open # Count=0 means open if items are later added
260
319
  end
261
- item.text_color(text_color) if text_color
262
- item.flag(*flags) if flags
263
- item[:Count] = 0 if open # Count=0 means open if items are later added
264
320
 
265
321
  unless position == :last || position == :first || position.kind_of?(Integer)
266
322
  raise ArgumentError, "position must be :first, :last, or an integer"
@@ -311,18 +367,20 @@ module HexaPDF
311
367
  end
312
368
 
313
369
  # :call-seq:
314
- # item.each_item {|descendant_item| block } -> item
315
- # item.each_item -> Enumerator
370
+ # item.each_item {|descendant_item, level| block } -> item
371
+ # item.each_item -> Enumerator
316
372
  #
317
373
  # Iterates over all descendant items of this one.
318
374
  #
319
- # The items are yielded in-order, yielding first the item itself and then its descendants.
375
+ # The descendant items are yielded in-order, yielding first the item itself and then its
376
+ # descendants.
320
377
  def each_item(&block)
321
378
  return to_enum(__method__) unless block_given?
379
+ return self unless (item = self[:First])
322
380
 
323
- item = self[:First]
381
+ level = self.level + 1
324
382
  while item
325
- yield(item)
383
+ yield(item, level)
326
384
  item.each_item(&block)
327
385
  item = item[:Next]
328
386
  end
@@ -341,7 +399,7 @@ module HexaPDF
341
399
  node, dir = first ? [first, :Next] : [last, :Prev]
342
400
  node = node[dir] while node.key?(dir)
343
401
  self[dir == :Next ? :Last : :First] = node
344
- elsif !first && !last && self[:Count] != 0
402
+ elsif !first && !last && self[:Count] && self[:Count] != 0
345
403
  yield('Outline item dictionary key /Count set but no descendants exist', true)
346
404
  delete(:Count)
347
405
  end
@@ -187,8 +187,8 @@ module HexaPDF
187
187
  end
188
188
 
189
189
  # :call-seq:
190
- # page.box(type = :media) -> box
191
- # page.box(type = :media, rectangle) -> rectangle
190
+ # page.box(type = :crop) -> box
191
+ # page.box(type = :crop, rectangle) -> rectangle
192
192
  #
193
193
  # If no +rectangle+ is given, returns the rectangle defining a certain kind of box for the
194
194
  # page. Otherwise sets the value for the given box type to +rectangle+ (an array with four
@@ -219,7 +219,7 @@ module HexaPDF
219
219
  # author. The default is the crop box.
220
220
  #
221
221
  # See: PDF1.7 s14.11.2
222
- def box(type = :media, rectangle = nil)
222
+ def box(type = :crop, rectangle = nil)
223
223
  if rectangle
224
224
  case type
225
225
  when :media, :crop, :bleed, :trim, :art
@@ -240,9 +240,10 @@ module HexaPDF
240
240
  end
241
241
  end
242
242
 
243
- # Returns the orientation of the media box, either :portrait or :landscape.
244
- def orientation
245
- box = self[:MediaBox]
243
+ # Returns the orientation of the specified box (default is the crop box), either :portrait or
244
+ # :landscape.
245
+ def orientation(type = :crop)
246
+ box = self.box(type)
246
247
  rotation = self[:Rotate]
247
248
  if (box.height > box.width && (rotation == 0 || rotation == 180)) ||
248
249
  (box.height < box.width && (rotation == 90 || rotation == 270))
@@ -272,21 +273,28 @@ module HexaPDF
272
273
  delete(:Rotate)
273
274
  return if cw_angle == 0
274
275
 
275
- matrix, llx, lly, urx, ury = \
276
- case cw_angle
277
- when 90
278
- [HexaPDF::Content::TransformationMatrix.new(0, -1, 1, 0),
279
- box.right, box.bottom, box.left, box.top]
280
- when 180
281
- [HexaPDF::Content::TransformationMatrix.new(-1, 0, 0, -1),
282
- box.right, box.top, box.left, box.bottom]
283
- when 270
284
- [HexaPDF::Content::TransformationMatrix.new(0, 1, -1, 0),
285
- box.left, box.top, box.right, box.bottom]
286
- end
287
- [:MediaBox, :CropBox, :BleedBox, :TrimBox, :ArtBox].each do |box|
288
- next unless key?(box)
289
- self[box].value = matrix.evaluate(llx, lly).concat(matrix.evaluate(urx, ury))
276
+ matrix = case cw_angle
277
+ when 90
278
+ HexaPDF::Content::TransformationMatrix.new(0, -1, 1, 0)
279
+ when 180
280
+ HexaPDF::Content::TransformationMatrix.new(-1, 0, 0, -1)
281
+ when 270
282
+ HexaPDF::Content::TransformationMatrix.new(0, 1, -1, 0)
283
+ end
284
+
285
+ [:MediaBox, :CropBox, :BleedBox, :TrimBox, :ArtBox].each do |box_name|
286
+ next unless key?(box_name)
287
+ box = self[box_name]
288
+ llx, lly, urx, ury = \
289
+ case cw_angle
290
+ when 90
291
+ [box.right, box.bottom, box.left, box.top]
292
+ when 180
293
+ [box.right, box.top, box.left, box.bottom]
294
+ when 270
295
+ [box.left, box.top, box.right, box.bottom]
296
+ end
297
+ self[box_name].value = matrix.evaluate(llx, lly).concat(matrix.evaluate(urx, ury))
290
298
  end
291
299
 
292
300
  before_contents = document.add({}, stream: " q #{matrix.to_a.join(' ')} cm ")
@@ -373,20 +381,33 @@ module HexaPDF
373
381
 
374
382
  # Returns the requested type of canvas for the page.
375
383
  #
376
- # The canvas object is cached once it is created so that its graphics state is correctly
377
- # retained without the need for parsing its contents.
378
- #
379
- # If the media box of the page doesn't have its origin at (0, 0), the canvas origin is
380
- # translated into the bottom left corner so that this detail doesn't matter when using the
381
- # canvas. This means that the canvas' origin is always at the bottom left corner of the media
382
- # box.
384
+ # There are potentially three different canvas objects, one for each of the types :underlay,
385
+ # :page, and :overlay. The canvas objects are cached once they are created so that their
386
+ # graphics states are correctly retained without the need for parsing the contents. This also
387
+ # means that on subsequent invocations the graphic states of the canvases might already be
388
+ # changed.
383
389
  #
384
390
  # type::
385
391
  # Can either be
386
392
  # * :page for getting the canvas for the page itself (only valid for initially empty pages)
387
393
  # * :overlay for getting the canvas for drawing over the page contents
388
394
  # * :underlay for getting the canvas for drawing unter the page contents
389
- def canvas(type: :page)
395
+ #
396
+ # translate_origin::
397
+ # Specifies whether the origin should automatically be translated into the lower-left
398
+ # corner of the crop box.
399
+ #
400
+ # Note that this argument is only used for the first invocation for every canvas type. So
401
+ # if a canvas was initially requested with this argument set to false and then with true,
402
+ # it won't have any effect as the cached canvas is returned.
403
+ #
404
+ # To check whether the origin has been translated or not, use
405
+ #
406
+ # canvas.graphics_state.ctm.evaluate(0, 0)
407
+ #
408
+ # and check whether the result is [0, 0]. If it is, then the origin has not been
409
+ # translated.
410
+ def canvas(type: :page, translate_origin: true)
390
411
  unless [:page, :overlay, :underlay].include?(type)
391
412
  raise ArgumentError, "Invalid value for 'type', expected: :page, :underlay or :overlay"
392
413
  end
@@ -399,9 +420,10 @@ module HexaPDF
399
420
 
400
421
  create_canvas = lambda do
401
422
  Content::Canvas.new(self).tap do |canvas|
402
- media_box = box(:media)
403
- if media_box.left != 0 || media_box.bottom != 0
404
- canvas.translate(media_box.left, media_box.bottom)
423
+ next unless translate_origin
424
+ crop_box = box(:crop)
425
+ if crop_box.left != 0 || crop_box.bottom != 0
426
+ canvas.translate(crop_box.left, crop_box.bottom)
405
427
  end
406
428
  end
407
429
  end
@@ -505,9 +527,8 @@ module HexaPDF
505
527
 
506
528
  canvas = self.canvas(type: :overlay)
507
529
  canvas.save_graphics_state
508
- media_box = box(:media)
509
- if media_box.left != 0 || media_box.bottom != 0
510
- canvas.translate(-media_box.left, -media_box.bottom) # revert initial translation of origin
530
+ if (pos = canvas.graphics_state.ctm.evaluate(0, 0)) != [0, 0]
531
+ canvas.translate(-pos[0], -pos[1])
511
532
  end
512
533
 
513
534
  to_delete = []
@@ -217,24 +217,20 @@ module HexaPDF
217
217
  # Ensures that a valid procedure set is available.
218
218
  def perform_validation
219
219
  super
220
- val = self[:ProcSet]
221
- if val
222
- if val.kind_of?(Symbol)
223
- yield("Procedure set is a single value instead of an Array", true)
224
- val = value[:ProcSet] = [val]
225
- end
226
- val.reject! do |name|
227
- case name
228
- when :PDF, :Text, :ImageB, :ImageC, :ImageI
229
- false
230
- else
231
- yield("Invalid page procedure set name /#{name}", true)
232
- true
233
- end
220
+ return unless (val = self[:ProcSet])
221
+
222
+ if val.kind_of?(Symbol)
223
+ yield("Procedure set is a single value instead of an Array", true)
224
+ val = value[:ProcSet] = [val]
225
+ end
226
+ val.reject! do |name|
227
+ case name
228
+ when :PDF, :Text, :ImageB, :ImageC, :ImageI
229
+ false
230
+ else
231
+ yield("Invalid page procedure set name /#{name}", true)
232
+ true
234
233
  end
235
- else
236
- yield("No procedure set specified", true)
237
- self[:ProcSet] = [:PDF, :Text, :ImageB, :ImageC, :ImageI]
238
234
  end
239
235
  end
240
236
 
@@ -104,8 +104,22 @@ module HexaPDF
104
104
  result.log(:error, "Certificate key usage is missing 'Digital Signature'")
105
105
  end
106
106
 
107
- if @pkcs7.verify(certificate_chain, store, signature_dict.signed_data,
108
- OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY)
107
+ if signature_dict.signature_type == 'ETSI.RFC3161'
108
+ # Getting the needed values is not directly supported by Ruby OpenSSL
109
+ p7 = OpenSSL::ASN1.decode(signature_dict.contents.sub(/\x00*\z/, ''))
110
+ signed_data = p7.value[1].value[0]
111
+ content_info = signed_data.value[2]
112
+ content = OpenSSL::ASN1.decode(content_info.value[1].value[0].value)
113
+ digest_algorithm = content.value[2].value[0].value[0].value
114
+ original_hash = content.value[2].value[1].value
115
+ recomputed_hash = OpenSSL::Digest.digest(digest_algorithm, signature_dict.signed_data)
116
+ hash_valid = (original_hash == recomputed_hash)
117
+ else
118
+ data = signature_dict.signed_data
119
+ hash_valid = true # hash will be checked by @pkcs7.verify
120
+ end
121
+ if hash_valid && @pkcs7.verify(certificate_chain, store, data,
122
+ OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY)
109
123
  result.log(:info, "Signature valid")
110
124
  else
111
125
  result.log(:error, "Signature verification failed")
@@ -230,6 +230,16 @@ module HexaPDF
230
230
  signature_handler.verify(store, allow_self_signed: allow_self_signed)
231
231
  end
232
232
 
233
+ private
234
+
235
+ def perform_validation #:nodoc:
236
+ if (self[:SubFilter] == :'ETSI.CAdES.detached' || self[:SubFilter] == :'ETSI.RFC3161') &&
237
+ document.version < '2.0'
238
+ yield("Signature handler needs at least PDF version 2.0", true)
239
+ document.version = '2.0'
240
+ end
241
+ end
242
+
233
243
  end
234
244
 
235
245
  end
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.26.2'
40
+ VERSION = '0.27.0'
41
41
 
42
42
  end
@@ -110,6 +110,9 @@ module HexaPDF
110
110
 
111
111
  revision = Revision.new(@document.revisions.current.trailer)
112
112
  @document.trailer.info[:Producer] = "HexaPDF version #{HexaPDF::VERSION}"
113
+ if parser.file_header_version < @document.version
114
+ @document.catalog[:Version] = @document.version.to_sym
115
+ end
113
116
  @document.revisions.each do |rev|
114
117
  rev.each_modified_object {|obj| revision.send(:add_without_check, obj) }
115
118
  end
@@ -29,6 +29,12 @@ describe HexaPDF::Document::Destinations::Destination do
29
29
  end
30
30
  end
31
31
 
32
+ it "accepts an array or a dictionary containing a /D entry as value" do
33
+ assert(destination([5, :Fit]).valid?)
34
+ assert(destination({D: [5, :Fit]}).valid?)
35
+ assert(destination(HexaPDF::Dictionary.new({D: [5, :Fit]})).valid?)
36
+ end
37
+
32
38
  it "can be asked whether the referenced page is in a remote document" do
33
39
  assert(destination([5, :Fit]).remote?)
34
40
  refute(destination([HexaPDF::Dictionary.new({}), :Fit]).remote?)
@@ -42,6 +48,10 @@ describe HexaPDF::Document::Destinations::Destination do
42
48
  assert(destination([5, :Fit]).valid?)
43
49
  end
44
50
 
51
+ it "returns the destination array" do
52
+ assert_equal([5, :Fit], destination([5, :Fit]).value)
53
+ end
54
+
45
55
  describe "type :xyz" do
46
56
  before do
47
57
  @dest = destination([:page, :XYZ, :left, :top, :zoom])
@@ -396,6 +406,37 @@ describe HexaPDF::Document::Destinations do
396
406
  refute(@doc.destinations['abc'])
397
407
  end
398
408
 
409
+ describe "resolve" do
410
+ it "resolves the named destination" do
411
+ @doc.catalog.names.destinations.add_entry("arr", [@page, :Fit])
412
+ @doc.catalog.names.destinations.add_entry("dict", {D: [@page, :Fit]})
413
+ assert_equal([@page, :Fit], @doc.destinations.resolve("arr").value)
414
+ assert_equal([@page, :Fit], @doc.destinations.resolve("dict").value)
415
+ end
416
+
417
+ it "returns nil if the named destination is not found" do
418
+ assert_nil(@doc.destinations.resolve("arr"))
419
+ end
420
+
421
+ it "resolves the old-style named destination" do
422
+ @doc.catalog[:Dests] = {arr: [@page, :Fit]}
423
+ assert_equal([@page, :Fit], @doc.destinations.resolve(:arr).value)
424
+ end
425
+
426
+ it "returns nil if the old-style named destination is not found" do
427
+ assert_nil(@doc.destinations.resolve(:arr))
428
+ end
429
+
430
+ it "uses a PDFArray or array argument directly" do
431
+ assert_equal([@page, :Fit], @doc.destinations.resolve([@page, :Fit]).value)
432
+ assert_equal([@page, :Fit], @doc.destinations.resolve(HexaPDF::PDFArray.new([@page, :Fit])).value)
433
+ end
434
+
435
+ it "returns nil if the resolved destination is not valid" do
436
+ assert_nil(@doc.destinations.resolve([@page, :Fitd]))
437
+ end
438
+ end
439
+
399
440
  describe "each" do
400
441
  before do
401
442
  3.times {|i| @doc.destinations.add("abc#{i}", [:page, :Fit]) }
@@ -19,17 +19,37 @@ describe HexaPDF::Document::Signatures do
19
19
  )
20
20
  end
21
21
 
22
- describe "DefaultHandler" do
23
- it "returns the filter name" do
24
- assert_equal(:'Adobe.PPKLite', @handler.filter_name)
25
- end
26
-
27
- it "returns the sub filter algorithm name" do
28
- assert_equal(:'adbe.pkcs7.detached', @handler.sub_filter_name)
22
+ it "allows embedding an external signature value" do
23
+ doc = HexaPDF::Document.new(io: StringIO.new(MINIMAL_PDF))
24
+ io = StringIO.new(''.b)
25
+ doc.signatures.add(io, @handler)
26
+ doc = HexaPDF::Document.new(io: io)
27
+ io = StringIO.new(''.b)
28
+
29
+ byte_range = nil
30
+ @handler.signature_size = 5000
31
+ @handler.external_signing = proc {|_, br| byte_range = br; "" }
32
+ doc.signatures.add(io, @handler)
33
+
34
+ io.pos = byte_range[0]
35
+ data = io.read(byte_range[1])
36
+ io.pos = byte_range[2]
37
+ data << io.read(byte_range[3])
38
+ contents = OpenSSL::PKCS7.sign(@handler.certificate, @handler.key, data, @handler.certificate_chain,
39
+ OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY).to_der
40
+ HexaPDF::Document::Signatures.embed_signature(io, contents)
41
+ doc = HexaPDF::Document.new(io: io)
42
+ assert_equal(2, doc.signatures.each.count)
43
+ doc.signatures.each do |signature|
44
+ assert(signature.verify(allow_self_signed: true).messages.find {|m| m.content == 'Signature valid' })
29
45
  end
46
+ end
30
47
 
48
+ describe "DefaultHandler" do
31
49
  it "returns the size of serialized signature" do
32
50
  assert_equal(1310, @handler.signature_size)
51
+ @handler.signature_size = 100
52
+ assert_equal(100, @handler.signature_size)
33
53
  end
34
54
 
35
55
  it "allows setting the DocMDP permissions" do
@@ -56,16 +76,23 @@ describe HexaPDF::Document::Signatures do
56
76
  assert_raises(ArgumentError) { @handler.doc_mdp_permissions = :other }
57
77
  end
58
78
 
59
- it "can sign the data using PKCS7" do
60
- data = "data"
61
- store = OpenSSL::X509::Store.new
62
- store.add_cert(CERTIFICATES.ca_certificate)
79
+ describe "sign" do
80
+ it "can sign the data using PKCS7" do
81
+ data = StringIO.new("data")
82
+ store = OpenSSL::X509::Store.new
83
+ store.add_cert(CERTIFICATES.ca_certificate)
84
+
85
+ pkcs7 = OpenSSL::PKCS7.new(@handler.sign(data, [0, 4, 0, 0]))
86
+ assert(pkcs7.detached?)
87
+ assert_equal([CERTIFICATES.signer_certificate, CERTIFICATES.ca_certificate],
88
+ pkcs7.certificates)
89
+ assert(pkcs7.verify([], store, data.string, OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY))
90
+ end
63
91
 
64
- pkcs7 = OpenSSL::PKCS7.new(@handler.sign(data))
65
- assert(pkcs7.detached?)
66
- assert_equal([CERTIFICATES.signer_certificate, CERTIFICATES.ca_certificate],
67
- pkcs7.certificates)
68
- assert(pkcs7.verify([], store, data, OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY))
92
+ it "can use external signing" do
93
+ @handler.external_signing = proc { "hallo" }
94
+ assert_equal("hallo", @handler.sign(StringIO.new, [0, 0, 0, 0]))
95
+ end
69
96
  end
70
97
 
71
98
  describe "finalize_objects" do
@@ -80,6 +107,12 @@ describe HexaPDF::Document::Signatures do
80
107
  assert(@obj.empty?)
81
108
  end
82
109
 
110
+ it "adjust the /SubFilter if signature type is etsi" do
111
+ @handler.signature_type = :etsi
112
+ @handler.finalize_objects(@field, @obj)
113
+ assert_equal(:'ETSI.CAdES.detached', @obj[:SubFilter])
114
+ end
115
+
83
116
  it "sets the reason, location and contact info fields" do
84
117
  @handler.reason = 'Reason'
85
118
  @handler.location = 'Location'
@@ -111,6 +144,87 @@ describe HexaPDF::Document::Signatures do
111
144
  end
112
145
  end
113
146
 
147
+ describe "TimestampHandler" do
148
+ before do
149
+ @handler = HexaPDF::Document::Signatures::TimestampHandler.new
150
+ end
151
+
152
+ it "allows setting the attributes in the constructor" do
153
+ handler = HexaPDF::Document::Signatures::TimestampHandler.new(
154
+ tsa_url: "url", tsa_hash_algorithm: "MD5", tsa_policy_id: "5",
155
+ reason: "Reason", location: "Location", contact_info: "Contact",
156
+ signature_size: 1_000
157
+ )
158
+ assert_equal("url", handler.tsa_url)
159
+ assert_equal("MD5", handler.tsa_hash_algorithm)
160
+ assert_equal("5", handler.tsa_policy_id)
161
+ assert_equal("Reason", handler.reason)
162
+ assert_equal("Location", handler.location)
163
+ assert_equal("Contact", handler.contact_info)
164
+ assert_equal(1_000, handler.signature_size)
165
+ end
166
+
167
+ it "finalizes the signature field and signature objects" do
168
+ @field = @doc.wrap({})
169
+ @sig = @doc.wrap({})
170
+ @handler.reason = 'Reason'
171
+ @handler.location = 'Location'
172
+ @handler.contact_info = 'Contact'
173
+
174
+ @handler.finalize_objects(@field, @sig)
175
+ assert_equal('2.0', @doc.version)
176
+ assert_equal(:DocTimeStamp, @sig[:Type])
177
+ assert_equal(:'ETSI.RFC3161', @sig[:SubFilter])
178
+ assert_equal('Reason', @sig[:Reason])
179
+ assert_equal('Location', @sig[:Location])
180
+ assert_equal('Contact', @sig[:ContactInfo])
181
+ end
182
+
183
+ it "returns the size of serialized signature" do
184
+ @handler.tsa_url = "http://127.0.0.1:34567"
185
+ CERTIFICATES.start_tsa_server
186
+ assert_equal(1420, @handler.signature_size)
187
+ end
188
+
189
+ describe "sign" do
190
+ before do
191
+ @data = StringIO.new("data")
192
+ @range = [0, 4, 0, 0]
193
+ @handler.tsa_url = "http://127.0.0.1:34567"
194
+ CERTIFICATES.start_tsa_server
195
+ end
196
+
197
+ it "respects the set hash algorithm and policy id" do
198
+ @handler.tsa_hash_algorithm = 'SHA256'
199
+ @handler.tsa_policy_id = '1.2.3.4.2'
200
+ token = OpenSSL::ASN1.decode(@handler.sign(@data, @range))
201
+ content = OpenSSL::ASN1.decode(token.value[1].value[0].value[2].value[1].value[0].value)
202
+ policy_id = content.value[1].value
203
+ digest_algorithm = content.value[2].value[0].value[0].value
204
+ assert_equal('SHA256', digest_algorithm)
205
+ assert_equal("1.2.3.4.2", policy_id)
206
+ end
207
+
208
+ it "returns the serialized timestamp token" do
209
+ token = OpenSSL::PKCS7.new(@handler.sign(@data, @range))
210
+ assert_equal(CERTIFICATES.ca_certificate.subject, token.signers[0].issuer)
211
+ assert_equal(CERTIFICATES.timestamp_certificate.serial, token.signers[0].serial)
212
+ end
213
+
214
+ it "fails if the timestamp token could not be created" do
215
+ @handler.tsa_hash_algorithm = 'SHA1'
216
+ msg = assert_raises(HexaPDF::Error) { @handler.sign(@data, @range) }
217
+ assert_match(/BAD_ALG/, msg.message)
218
+ end
219
+
220
+ it "fails if the timestamp server couldn't process the request" do
221
+ @handler.tsa_policy_id = '1.2.3.4.1'
222
+ msg = assert_raises(HexaPDF::Error) { @handler.sign(@data, @range) }
223
+ assert_match(/Invalid TSA server response/, msg.message)
224
+ end
225
+ end
226
+ end
227
+
114
228
  it "iterates over all signature dictionaries" do
115
229
  assert_equal([], @doc.signatures.to_a)
116
230
  @sig1.field_value = :sig1
@@ -164,7 +278,7 @@ describe HexaPDF::Document::Signatures do
164
278
  sig = @doc.signatures.first
165
279
  assert_equal(:'Adobe.PPKLite', sig[:Filter])
166
280
  assert_equal(:'adbe.pkcs7.detached', sig[:SubFilter])
167
- assert_equal([0, 968, 3590, 2517], sig[:ByteRange].value)
281
+ assert_equal([0, 996, 3618, 2501], sig[:ByteRange].value)
168
282
  assert_equal(:sig, sig[:key])
169
283
  assert_equal(:sig_field, @doc.acro_form.each_field.first[:key])
170
284
  assert(sig.key?(:Contents))
@@ -205,14 +319,14 @@ describe HexaPDF::Document::Signatures do
205
319
  it "handles different xref section types correctly when determing the offsets" do
206
320
  @doc.delete(7)
207
321
  sig = @doc.signatures.add(@io, @handler, write_options: {update_fields: false})
208
- assert_equal([0, 968, 3590, 2491], sig[:ByteRange].value)
322
+ assert_equal([0, 988, 3610, 2483], sig[:ByteRange].value)
209
323
  end
210
324
 
211
325
  it "works if the signature object is the last object of the xref section" do
212
326
  field = @doc.acro_form(create: true).create_signature_field('Signature2')
213
327
  field.create_widget(@doc.pages[0], Rect: [0, 0, 0, 0])
214
328
  sig = @doc.signatures.add(@io, @handler, signature: field, write_options: {update_fields: false})
215
- assert_equal([0, 3063, 5685, 400], sig[:ByteRange].value)
329
+ assert_equal([0, 3095, 5717, 380], sig[:ByteRange].value)
216
330
  end
217
331
 
218
332
  it "allows writing to a file in addition to writing to an IO" do
@@ -228,5 +342,11 @@ describe HexaPDF::Document::Signatures do
228
342
  signed_doc = HexaPDF::Document.new(io: @io)
229
343
  assert(signed_doc.signatures.first.verify)
230
344
  end
345
+
346
+ it "fails if the reserved signature space is too small" do
347
+ def @handler.signature_size; 200; end
348
+ msg = assert_raises(HexaPDF::Error) { @doc.signatures.add(@io, @handler) }
349
+ assert_match(/space.*too small.*200 vs/, msg.message)
350
+ end
231
351
  end
232
352
  end