hexapdf 0.26.2 → 0.27.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|