hexapdf 0.26.2 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +56 -0
- data/README.md +1 -1
- data/examples/013-text_layouter_shapes.rb +8 -8
- data/examples/016-frame_automatic_box_placement.rb +3 -3
- data/examples/017-frame_text_flow.rb +3 -3
- data/examples/020-column_box.rb +3 -3
- data/lib/hexapdf/cli/split.rb +7 -7
- data/lib/hexapdf/cli/watermark.rb +2 -2
- data/lib/hexapdf/configuration.rb +2 -0
- data/lib/hexapdf/dictionary.rb +3 -12
- data/lib/hexapdf/document/destinations.rb +42 -5
- data/lib/hexapdf/document/signatures.rb +265 -48
- data/lib/hexapdf/importer.rb +3 -0
- data/lib/hexapdf/parser.rb +1 -0
- data/lib/hexapdf/revisions.rb +3 -1
- data/lib/hexapdf/tokenizer.rb +2 -2
- data/lib/hexapdf/type/acro_form/form.rb +28 -1
- data/lib/hexapdf/type/catalog.rb +1 -1
- data/lib/hexapdf/type/outline.rb +18 -0
- data/lib/hexapdf/type/outline_item.rb +72 -14
- data/lib/hexapdf/type/page.rb +56 -35
- data/lib/hexapdf/type/resources.rb +13 -17
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +16 -2
- data/lib/hexapdf/type/signature.rb +10 -0
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +3 -0
- data/test/hexapdf/document/test_destinations.rb +41 -0
- data/test/hexapdf/document/test_signatures.rb +139 -19
- data/test/hexapdf/test_importer.rb +14 -0
- data/test/hexapdf/test_parser.rb +2 -2
- data/test/hexapdf/test_revisions.rb +20 -12
- data/test/hexapdf/test_tokenizer.rb +11 -1
- data/test/hexapdf/test_writer.rb +11 -3
- data/test/hexapdf/type/acro_form/test_form.rb +47 -0
- data/test/hexapdf/type/signature/common.rb +52 -0
- data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +21 -0
- data/test/hexapdf/type/test_catalog.rb +5 -2
- data/test/hexapdf/type/test_outline.rb +1 -1
- data/test/hexapdf/type/test_outline_item.rb +62 -1
- data/test/hexapdf/type/test_page.rb +41 -20
- data/test/hexapdf/type/test_resources.rb +0 -5
- data/test/hexapdf/type/test_signature.rb +8 -0
- data/test/test_helper.rb +1 -1
- 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
|
-
|
255
|
-
|
256
|
-
|
257
|
-
item.
|
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.
|
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
|
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
|
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
|
-
|
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
|
data/lib/hexapdf/type/page.rb
CHANGED
@@ -187,8 +187,8 @@ module HexaPDF
|
|
187
187
|
end
|
188
188
|
|
189
189
|
# :call-seq:
|
190
|
-
# page.box(type = :
|
191
|
-
# page.box(type = :
|
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 = :
|
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
|
244
|
-
|
245
|
-
|
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
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
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
|
-
#
|
377
|
-
#
|
378
|
-
#
|
379
|
-
#
|
380
|
-
#
|
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
|
-
|
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
|
-
|
403
|
-
|
404
|
-
|
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
|
-
|
509
|
-
|
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
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
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
|
108
|
-
|
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
|
data/lib/hexapdf/version.rb
CHANGED
data/lib/hexapdf/writer.rb
CHANGED
@@ -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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
60
|
-
data
|
61
|
-
|
62
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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,
|
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,
|
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,
|
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
|